1)Profile ui and api integration done
This commit is contained in:
parent
a4160498e1
commit
e40371ad6a
@ -1,5 +1,9 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "com.android.application"
|
id "com.android.application"
|
||||||
|
// START: FlutterFire Configuration
|
||||||
|
id 'com.google.gms.google-services'
|
||||||
|
id 'com.google.firebase.crashlytics'
|
||||||
|
// END: FlutterFire Configuration
|
||||||
id "kotlin-android"
|
id "kotlin-android"
|
||||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
id "dev.flutter.flutter-gradle-plugin"
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
|
@ -24,6 +24,12 @@
|
|||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"
|
android:resource="@style/NormalTheme"
|
||||||
/>
|
/>
|
||||||
|
<meta-data
|
||||||
|
android:name="firebase_messaging_auto_init_enabled"
|
||||||
|
android:value="false" />
|
||||||
|
<meta-data
|
||||||
|
android:name="firebase_analytics_collection_enabled"
|
||||||
|
android:value="false" />
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
@ -3,21 +3,113 @@ import 'package:cheminova/models/user_model.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../notification_service.dart';
|
||||||
|
|
||||||
class HomeController extends GetxController {
|
class HomeController extends GetxController {
|
||||||
final HomeService homeService = HomeService();
|
final HomeService homeService = HomeService();
|
||||||
|
NotificationServices notificationServices = NotificationServices();
|
||||||
|
|
||||||
var user = Rxn<UserModel>();
|
UserModel? user;
|
||||||
|
// var userModel = UserModel(
|
||||||
|
// id: '',
|
||||||
|
// uniqueId: '',
|
||||||
|
// name: '',
|
||||||
|
// email: '',
|
||||||
|
// phone: '',
|
||||||
|
// role: '',
|
||||||
|
// sbu: '',
|
||||||
|
// createdAt: '',
|
||||||
|
// updatedAt: '',
|
||||||
|
// ).obs; // Observable for UserModel
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
getUser();
|
getUser();
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
notificationServices.requestNotificationPermission();
|
||||||
|
notificationServices.getDeviceToken().then((value) {
|
||||||
|
print('Device Token: $value');
|
||||||
|
fcmToken();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getUser() async
|
Future<void> fcmToken() async {
|
||||||
{
|
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
String? token = prefs.getString('token');
|
String? token = prefs.getString('token');
|
||||||
user.value = (await homeService.getUser(token: token)) as UserModel? ;
|
final fcmToken = await NotificationServices().getDeviceToken();
|
||||||
|
print('fcmToken: $fcmToken');
|
||||||
|
homeService.fcmToken({"fcmToken": fcmToken}, token!);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> getUser() async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
String? token = prefs.getString('token');
|
||||||
|
|
||||||
|
HomeService homeService = HomeService();
|
||||||
|
user = await homeService.getUser(token: token);
|
||||||
|
|
||||||
|
if (user != null) {
|
||||||
|
print(user); // For debugging, prints the user details
|
||||||
|
} else {
|
||||||
|
print('Failed to fetch user data');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// import 'package:cheminova/controller/home_service.dart';
|
||||||
|
// import 'package:cheminova/models/user_model.dart';
|
||||||
|
// import 'package:get/get.dart';
|
||||||
|
// import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
//
|
||||||
|
// import '../notification_service.dart';
|
||||||
|
//
|
||||||
|
// class HomeController extends GetxController {
|
||||||
|
// final HomeService homeService = HomeService();
|
||||||
|
// NotificationServices notificationServices = NotificationServices();
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// var userModel = UserModel(id: '', uniqueId: '', name: '', email: '', phone: '', role: '', sbu: '', createdAt: '', updatedAt: ''
|
||||||
|
//
|
||||||
|
// ).obs; // Observable for UserModel
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// void onInit() {
|
||||||
|
// getUser();
|
||||||
|
// super.onInit();
|
||||||
|
// notificationServices.requestNotificationPermission();
|
||||||
|
// notificationServices.getDeviceToken().then((value) {
|
||||||
|
// print('Device Token: $value');
|
||||||
|
// fcmToken();
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Future<void> fcmToken() async {
|
||||||
|
// SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
// String? token = prefs.getString('token');
|
||||||
|
// final fcmToken = await NotificationServices().getDeviceToken();
|
||||||
|
// print('fcmToken: $fcmToken');
|
||||||
|
// homeService.fcmToken({"fcmToken": fcmToken}, token!);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Future<void> getUser() async {
|
||||||
|
// SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
//
|
||||||
|
// String? token = prefs.getString('token');
|
||||||
|
//
|
||||||
|
// userModel = (await homeService.getUser(token: token)) as dynamic;
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// // if (userModel != null) {
|
||||||
|
// // if (userModel != null) {ddddd
|
||||||
|
// // userModel.value = userResponse as UserModel; // Update the userModel with API response
|
||||||
|
// // update(); // Notify GetX to rebuild widgets using GetBuilder/Obx
|
||||||
|
// // }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
@ -1,25 +1,58 @@
|
|||||||
import 'package:cheminova/models/user_model.dart';
|
import 'package:cheminova/models/user_model.dart';
|
||||||
|
import 'package:cheminova/utils/api_urls.dart';
|
||||||
import 'package:cheminova/utils/common_api_service.dart';
|
import 'package:cheminova/utils/common_api_service.dart';
|
||||||
import 'package:cheminova/utils/show_snackbar.dart';
|
import 'package:cheminova/utils/show_snackbar.dart';
|
||||||
|
|
||||||
class HomeService {
|
class HomeService {
|
||||||
Future<Map<String, dynamic>?> getUser({String? token}) async {
|
Future<UserModel?> getUser({String? token}) async {
|
||||||
try {
|
try {
|
||||||
final response = await commonApiService<Map<String,dynamic>>(
|
final response = await commonApiService<UserModel>(
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "api/v1/user/details",
|
url: ApiUrls.profileUrl,
|
||||||
fromJson: (json) => json,
|
|
||||||
additionalHeaders: { // Pass the token here
|
additionalHeaders: { // Pass the token here
|
||||||
'Authorization': 'Bearer $token',
|
'Authorization': 'Bearer $token',
|
||||||
},
|
},
|
||||||
|
fromJson: (json) {
|
||||||
|
if (json['user'] != null) {
|
||||||
|
// Parse the user data from the API response
|
||||||
|
return UserModel.fromJson(json['user']);
|
||||||
|
}
|
||||||
|
return json as UserModel;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
print(e.toString());
|
||||||
|
showSnackbar(e.toString()); // Optional: show error using a Snackbar
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> fcmToken(Map<String, dynamic> data,
|
||||||
|
String? token) async {
|
||||||
|
try {
|
||||||
|
final response = await commonApiService<String>(
|
||||||
|
url: ApiUrls.fcmUrl,
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
fromJson: (json) => json as String,
|
||||||
|
// Just return the string response
|
||||||
|
additionalHeaders: { // Pass the token here
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return response;
|
if (response != null) {
|
||||||
|
// Since the response is a string, wrap it in a Map to avoid type issues
|
||||||
|
return {
|
||||||
|
'message': response
|
||||||
|
}; // Return the response in a map with 'message' as the key
|
||||||
|
}
|
||||||
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showSnackbar(e.toString());
|
showSnackbar(e.toString()); // Handle any errors
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,43 +1,99 @@
|
|||||||
class UserModel {
|
class UserModel {
|
||||||
String id;
|
final String id;
|
||||||
String name;
|
final String uniqueId;
|
||||||
String uniqueId;
|
final String name;
|
||||||
String email;
|
final String email;
|
||||||
bool isVerified;
|
final String phone;
|
||||||
|
final String role;
|
||||||
|
final String sbu;
|
||||||
|
final String createdAt;
|
||||||
|
final String updatedAt;
|
||||||
|
final String? fcmToken;
|
||||||
|
final Avatar? avatar;
|
||||||
|
|
||||||
UserModel({
|
UserModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
|
||||||
required this.uniqueId,
|
required this.uniqueId,
|
||||||
|
required this.name,
|
||||||
required this.email,
|
required this.email,
|
||||||
required this.isVerified,
|
required this.phone,
|
||||||
|
required this.role,
|
||||||
|
required this.sbu,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
this.fcmToken,
|
||||||
|
this.avatar,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Factory constructor to create an instance of UserModel from a JSON map
|
// Factory constructor for converting JSON to UserModel
|
||||||
factory UserModel.fromJson(Map<String, dynamic> json) {
|
factory UserModel.fromJson(Map<String, dynamic> json) {
|
||||||
return UserModel(
|
return UserModel(
|
||||||
id: json['_id'] ??"",
|
id: json['_id'],
|
||||||
name: json['name'] ??"Sarita",
|
uniqueId: json['uniqueId'],
|
||||||
uniqueId: json['uniqueId'] ??"1234",
|
name: json['name'],
|
||||||
email: json['email'] ??"",
|
email: json['email'],
|
||||||
isVerified: json['isVerified'] as bool? ??false,
|
phone: json['phone'],
|
||||||
|
role: json['role'],
|
||||||
|
sbu: json['SBU'],
|
||||||
|
createdAt: json['createdAt'],
|
||||||
|
updatedAt: json['updatedAt'],
|
||||||
|
fcmToken: json['fcm_token'],
|
||||||
|
avatar: json['avatar'] != null ? Avatar.fromJson(json['avatar']) : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method to convert an instance of UserModel to a JSON map
|
// Method to convert UserModel to JSON
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
'_id': id,
|
'_id': id,
|
||||||
'name': name,
|
|
||||||
'uniqueId': uniqueId,
|
'uniqueId': uniqueId,
|
||||||
|
'name': name,
|
||||||
'email': email,
|
'email': email,
|
||||||
'isVerified': isVerified,
|
'phone': phone,
|
||||||
|
'role': role,
|
||||||
|
'SBU': sbu,
|
||||||
|
'createdAt': createdAt,
|
||||||
|
'updatedAt': updatedAt,
|
||||||
|
'fcm_token': fcmToken,
|
||||||
|
'avatar': avatar?.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override toString() to provide a readable output
|
// Overriding toString to get a readable output for debugging
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'UserModel{id: $id, name: $name, uniqueId: $uniqueId, email: $email, isVerified: $isVerified}';
|
return 'UserModel{id: $id, uniqueId: $uniqueId, name: $name, email: $email, phone: $phone, role: $role, sbu: $sbu, createdAt: $createdAt, updatedAt: $updatedAt, fcmToken: $fcmToken, avatar: $avatar}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avatar model to handle avatar data
|
||||||
|
class Avatar {
|
||||||
|
final String publicId;
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
Avatar({
|
||||||
|
required this.publicId,
|
||||||
|
required this.url,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Factory constructor for converting JSON to Avatar
|
||||||
|
factory Avatar.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Avatar(
|
||||||
|
publicId: json['public_id'] ?? '',
|
||||||
|
url: json['url'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to convert Avatar to JSON
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'public_id': publicId,
|
||||||
|
'url': url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Avatar{publicId: $publicId, url: $url}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:cheminova/controller/home_controller.dart';
|
||||||
import 'package:cheminova/screens/authentication/controller/auth_service.dart';
|
import 'package:cheminova/screens/authentication/controller/auth_service.dart';
|
||||||
import 'package:cheminova/screens/home_screen.dart';
|
import 'package:cheminova/screens/home_screen.dart';
|
||||||
import 'package:cheminova/utils/show_snackbar.dart';
|
import 'package:cheminova/utils/show_snackbar.dart';
|
||||||
@ -5,18 +8,31 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../../../notification_service.dart';
|
||||||
|
import '../../../utils/api_client.dart';
|
||||||
|
import '../../../utils/api_urls.dart';
|
||||||
|
import '../../../utils/secure__storage_service.dart';
|
||||||
|
|
||||||
class AuthController extends GetxController {
|
class AuthController extends GetxController {
|
||||||
final authService = AuthService();
|
final authService = AuthService();
|
||||||
|
|
||||||
|
final _apiClient = ApiClient();
|
||||||
|
final _storageService = SecureStorageService();
|
||||||
|
|
||||||
TextEditingController emailController = TextEditingController();
|
TextEditingController emailController = TextEditingController();
|
||||||
TextEditingController passwordController = TextEditingController();
|
TextEditingController passwordController = TextEditingController();
|
||||||
TextEditingController phoneController = TextEditingController();
|
//TextEditingController phoneController = TextEditingController();
|
||||||
TextEditingController currentpassController = TextEditingController();
|
TextEditingController currentpassController = TextEditingController();
|
||||||
TextEditingController newpassController = TextEditingController();
|
TextEditingController newpassController = TextEditingController();
|
||||||
TextEditingController confirmpassController = TextEditingController();
|
TextEditingController confirmpassController = TextEditingController();
|
||||||
|
final HomeController _homeController = Get.put(HomeController());
|
||||||
|
|
||||||
RxBool isLoading = false.obs;
|
RxBool isLoading = false.obs;
|
||||||
|
@override
|
||||||
|
void onInit(){
|
||||||
|
super.onInit();
|
||||||
|
NotificationServices().requestNotificationPermission();
|
||||||
|
}
|
||||||
Future<void> login() async {
|
Future<void> login() async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
final response = await authService.login({
|
final response = await authService.login({
|
||||||
@ -26,7 +42,9 @@ class AuthController extends GetxController {
|
|||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
update();
|
update();
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
|
_homeController.fcmToken();
|
||||||
showSnackbar("Your Successfully logged In!");
|
showSnackbar("Your Successfully logged In!");
|
||||||
|
|
||||||
Get.offAll(() => const HomeScreen());
|
Get.offAll(() => const HomeScreen());
|
||||||
}
|
}
|
||||||
else if(response == null){
|
else if(response == null){
|
||||||
@ -34,7 +52,54 @@ class AuthController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> forgotpass() async{
|
// Future<(bool, String)> login() async {
|
||||||
|
// isLoading(true);
|
||||||
|
// try {
|
||||||
|
// Response response = (await _apiClient.post(
|
||||||
|
// ApiUrls.loginUrl,
|
||||||
|
// data: {
|
||||||
|
// 'email': emailController.text,
|
||||||
|
// 'password': passwordController.text
|
||||||
|
// },
|
||||||
|
// )) as Response;
|
||||||
|
//
|
||||||
|
// isLoading(false);
|
||||||
|
//
|
||||||
|
// // Check if the response status code is 200
|
||||||
|
// if (response.statusCode == 200) {
|
||||||
|
// // Access the data field from the response
|
||||||
|
// final responseData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
// final token = responseData['token'];
|
||||||
|
// final message = responseData['message'];
|
||||||
|
//
|
||||||
|
// await _storageService.write(
|
||||||
|
// key: 'access_token',
|
||||||
|
// value: token,
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// final fcmToken = await NotificationServices().getDeviceToken();
|
||||||
|
// print('fcmToken: $fcmToken');
|
||||||
|
//
|
||||||
|
// await _apiClient.post(
|
||||||
|
// ApiUrls.fcmUrl,
|
||||||
|
// data: {'fcmToken': fcmToken},
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// return (true, message.toString());
|
||||||
|
// } else {
|
||||||
|
// // Handle the error based on status code
|
||||||
|
// final responseData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
// return (false, responseData['message'].toString());
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// isLoading(false);
|
||||||
|
// return (false, 'Something went wrong');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Future<void> forgotpass() async{
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
final response = await authService.forgotPassword(
|
final response = await authService.forgotPassword(
|
||||||
{
|
{
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
|
import 'package:cheminova/utils/api_urls.dart';
|
||||||
import 'package:cheminova/utils/common_api_service.dart';
|
import 'package:cheminova/utils/common_api_service.dart';
|
||||||
import 'package:cheminova/utils/show_snackbar.dart';
|
import 'package:cheminova/utils/show_snackbar.dart';
|
||||||
|
|
||||||
|
import '../../../utils/api_client.dart';
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
|
|
||||||
Future<Map<String, dynamic>?> login(Map<String, dynamic> data) async {
|
Future<Map<String, dynamic>?> login(Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
final response = await commonApiService<Map<String, dynamic>>(
|
final response = await commonApiService<Map<String, dynamic>>(
|
||||||
url: '/api/v1/user/login/',
|
url: ApiUrls.loginUrl,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: data,
|
body: data,
|
||||||
fromJson: (json) => json, // Simply return the JSON map as is
|
fromJson: (json) => json, // Simply return the JSON map as is
|
||||||
@ -21,7 +25,7 @@ class AuthService {
|
|||||||
Future<Map<String, dynamic>?> forgotPassword(Map<String, dynamic> data) async {
|
Future<Map<String, dynamic>?> forgotPassword(Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
final response = await commonApiService<Map<String, dynamic>>(
|
final response = await commonApiService<Map<String, dynamic>>(
|
||||||
url: '/api/v1/user/password/forgot',
|
url: ApiUrls.forgetPasswordUrl,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: data,
|
body: data,
|
||||||
fromJson: (json) => json, // Simply return the JSON map as is
|
fromJson: (json) => json, // Simply return the JSON map as is
|
||||||
@ -37,7 +41,7 @@ class AuthService {
|
|||||||
Future<Map<String, dynamic>?> changePassword(Map<String, dynamic> data, {required String token}) async {
|
Future<Map<String, dynamic>?> changePassword(Map<String, dynamic> data, {required String token}) async {
|
||||||
try {
|
try {
|
||||||
final response = await commonApiService<Map<String, dynamic>>(
|
final response = await commonApiService<Map<String, dynamic>>(
|
||||||
url: '/api/v1/user/password/update',
|
url: ApiUrls.changePasswordUrl,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: data,
|
body: data,
|
||||||
fromJson: (json) => json, // Simply return the JSON map as is
|
fromJson: (json) => json, // Simply return the JSON map as is
|
||||||
|
117
lib/screens/authentication/profile_screen.dart
Normal file
117
lib/screens/authentication/profile_screen.dart
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import 'package:cheminova/controller/home_controller.dart';
|
||||||
|
import 'package:cheminova/widgets/my_drawer.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
|
||||||
|
import '../../widgets/comman_background.dart';
|
||||||
|
import '../../widgets/common_appbar.dart';
|
||||||
|
|
||||||
|
class ProfileScreen extends StatefulWidget {
|
||||||
|
// String? name;
|
||||||
|
// final String uniqueId;
|
||||||
|
// final String email;
|
||||||
|
// final String mobileNumber;
|
||||||
|
// final String designation;
|
||||||
|
|
||||||
|
const ProfileScreen({
|
||||||
|
Key? key,
|
||||||
|
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ProfileScreen> createState() => _ProfileScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProfileScreenState extends State<ProfileScreen> {
|
||||||
|
|
||||||
|
final homecontroller = Get.put(HomeController());
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final user = homecontroller!.user;
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
CommonBackground(
|
||||||
|
isFullWidth: true,
|
||||||
|
child: Scaffold(
|
||||||
|
drawer: MyDrawer(),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
appBar: CommonAppBar(
|
||||||
|
title: const Text('Profile'),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
icon: SvgPicture.asset(
|
||||||
|
'assets/svg/back_arrow.svg',
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.only(right: 20),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20.0)
|
||||||
|
.copyWith(top: 15, bottom: 30),
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 30.0, vertical: 20.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: Colors.white),
|
||||||
|
color: const Color(0xffB4D1E5).withOpacity(0.9),
|
||||||
|
borderRadius: BorderRadius.circular(26.0),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildProfileItem('Name', user!.name),
|
||||||
|
_buildProfileItem('ID', user.uniqueId),
|
||||||
|
_buildProfileItem('Email ID', user.email),
|
||||||
|
_buildProfileItem('Mobile Number', user.phone),
|
||||||
|
_buildProfileItem('Designation', user.role),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildProfileItem(String label, String value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xff004791),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(color: Colors.grey),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
60
lib/utils/api_client.dart
Normal file
60
lib/utils/api_client.dart
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
|
||||||
|
import 'package:cheminova/utils/secure__storage_service.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||||
|
|
||||||
|
import 'api_urls.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class ApiClient {
|
||||||
|
final Dio _dio;
|
||||||
|
final SecureStorageService _storageService = SecureStorageService();
|
||||||
|
|
||||||
|
ApiClient({String? baseUrl})
|
||||||
|
: _dio = Dio(BaseOptions(
|
||||||
|
baseUrl: baseUrl ?? ApiUrls.baseUrl,
|
||||||
|
connectTimeout: const Duration(seconds: 120),
|
||||||
|
receiveTimeout: const Duration(seconds: 120))) {
|
||||||
|
_dio.interceptors
|
||||||
|
.add(LogInterceptor(responseBody: true, requestBody: true));
|
||||||
|
_dio.interceptors
|
||||||
|
.add(InterceptorsWrapper(onRequest: (options, handler) async {
|
||||||
|
String? token = await _storageService.read(key: 'access_token');
|
||||||
|
if (token != null) {
|
||||||
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
|
}
|
||||||
|
_dio.interceptors
|
||||||
|
.add(LogInterceptor(responseBody: true, requestBody: true));
|
||||||
|
_dio.interceptors.add(PrettyDioLogger(
|
||||||
|
requestHeader: true,
|
||||||
|
requestBody: true,
|
||||||
|
responseBody: true,
|
||||||
|
responseHeader: false,
|
||||||
|
error: true,
|
||||||
|
compact: true,
|
||||||
|
maxWidth: 90,
|
||||||
|
));
|
||||||
|
return handler.next(options);
|
||||||
|
}, onResponse: (response, handler) {
|
||||||
|
return handler.next(response);
|
||||||
|
}, onError: (DioException e, handler) {
|
||||||
|
return handler.next(e);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> get(String path, {Map<String, dynamic>? queryParameters}) {
|
||||||
|
return _dio.get(path, queryParameters: queryParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Response> post(String path, {dynamic data}) {
|
||||||
|
return _dio.post(path, data: data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Response> put(String path, {Map<String, dynamic>? data}) {
|
||||||
|
return _dio.put(path, data: data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Response> delete(String path, {Map<String, dynamic>? data}) {
|
||||||
|
return _dio.delete(path, data: data);
|
||||||
|
}
|
||||||
|
}
|
18
lib/utils/api_urls.dart
Normal file
18
lib/utils/api_urls.dart
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
class ApiUrls {
|
||||||
|
// static const String baseUrl = 'https://cheminova-api-2.onrender.com/api/';
|
||||||
|
static const String baseUrl = 'https://api.cnapp.co.in';
|
||||||
|
static const String loginUrl = '/api/v1/user/login/';
|
||||||
|
static const String profileUrl = '/api/v1/user/details';
|
||||||
|
static const String forgetPasswordUrl = '/api/v1/user/password/forgot';
|
||||||
|
static const String changePasswordUrl = '/api/v1/user/password/update';
|
||||||
|
static const String fcmUrl = '/api/v1/user/fcm-token';
|
||||||
|
static const String getCategoryUrl = '/api/category/getCategories';
|
||||||
|
static const String getProductUrl = '/api/category/getCategories';
|
||||||
|
static const String getProductManualUrl = '/api/productmanual/getall';
|
||||||
|
static const String getKycUrl = '/api/kyc/getAll';
|
||||||
|
static const String getNotificationUrl = '/api/get-notification-pd';
|
||||||
|
static const String getPlacedOrderUrl ='/api/get-placed-order-pd';
|
||||||
|
static const String getSinglePlacedOrderUrl ='/api/get-single-placed-order-pd';
|
||||||
|
static const String placedOrderUrl ='${baseUrl}/api/order-place';
|
||||||
|
|
||||||
|
}
|
11
lib/utils/upper_case_format.dart
Normal file
11
lib/utils/upper_case_format.dart
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class UpperCaseTextFormatter extends TextInputFormatter {
|
||||||
|
@override
|
||||||
|
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
|
||||||
|
return newValue.copyWith(
|
||||||
|
text: newValue.text.toUpperCase(),
|
||||||
|
selection: newValue.selection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,83 +1,185 @@
|
|||||||
import 'package:cheminova/controller/home_controller.dart';
|
import 'package:cheminova/controller/home_controller.dart';
|
||||||
|
import 'package:cheminova/models/user_model.dart';
|
||||||
import 'package:cheminova/screens/authentication/change_password_screen.dart';
|
import 'package:cheminova/screens/authentication/change_password_screen.dart';
|
||||||
import 'package:cheminova/screens/authentication/login_screen.dart';
|
import 'package:cheminova/screens/authentication/login_screen.dart';
|
||||||
|
import 'package:cheminova/screens/authentication/profile_screen.dart';
|
||||||
import 'package:cheminova/screens/home_screen.dart';
|
import 'package:cheminova/screens/home_screen.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
class MyDrawer extends StatefulWidget {
|
class MyDrawer extends StatefulWidget {
|
||||||
const MyDrawer({super.key});
|
final UserModel? userModel;
|
||||||
|
|
||||||
|
MyDrawer({super.key, this.userModel});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MyDrawer> createState() => _MyDrawerState();
|
State<MyDrawer> createState() => _MyDrawerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MyDrawerState extends State<MyDrawer> {
|
class _MyDrawerState extends State<MyDrawer> {
|
||||||
final homeController = Get.put(HomeController());
|
final homecontroller = Get.put(HomeController());
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final user = homecontroller.user;
|
||||||
return Drawer(
|
return Drawer(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 150,
|
height: 150,
|
||||||
child: Obx(
|
child: DrawerHeader(
|
||||||
(){
|
decoration: const BoxDecoration(
|
||||||
return DrawerHeader(
|
color: Colors.black87,
|
||||||
decoration: const BoxDecoration(
|
),
|
||||||
color: Colors.black87,
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
user!.name ?? "username",
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
Text(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
user!.uniqueId ?? 'Employee ID',
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
style: const TextStyle(
|
||||||
children: [
|
color: Colors.white,
|
||||||
Text(
|
fontSize: 20,
|
||||||
homeController.user.value?.name?? "username",
|
),
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 18,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
homeController.user.value?.uniqueId?? 'Employee ID',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
],
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.home),
|
leading: const Icon(Icons.home),
|
||||||
title: const Text('Home'),
|
title: const Text('Home'),
|
||||||
onTap: () => Get.offAll(() => const HomeScreen()),
|
onTap: () {
|
||||||
|
Navigator.pushAndRemoveUntil(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) => const HomeScreen()),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.account_circle),
|
leading: const Icon(Icons.account_circle),
|
||||||
title: const Text('Profile'),
|
title: const Text('Profile'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) => ProfileScreen()),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.settings),
|
leading: const Icon(Icons.settings),
|
||||||
title: const Text('Change Password'),
|
title: const Text('Change Password'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Get.to(ChangePasswordScreen());
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) => ChangePasswordScreen()),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.exit_to_app),
|
leading: const Icon(Icons.exit_to_app),
|
||||||
title: const Text('Logout'),
|
title: const Text('Logout'),
|
||||||
onTap: () => Get.offAll(() => const LoginScreen()),
|
onTap: () {
|
||||||
|
logoutBox(context);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future logoutBox(BuildContext context) {
|
||||||
|
Size size = MediaQuery.of(context).size;
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6.0),
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
title: const Text('Are you sure you want to log out?'),
|
||||||
|
titleTextStyle: const TextStyle(
|
||||||
|
fontSize: 20, fontWeight: FontWeight.w400, color: Colors.black),
|
||||||
|
actionsAlignment: MainAxisAlignment.center,
|
||||||
|
actionsPadding: const EdgeInsets.only(left: 10, right: 10, bottom: 20),
|
||||||
|
actions: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SizedBox(
|
||||||
|
height: (size.height / 50.52) * 2,
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ButtonStyle(
|
||||||
|
elevation: MaterialStateProperty.all(0),
|
||||||
|
backgroundColor: MaterialStateProperty.all<Color>(
|
||||||
|
Colors.black),
|
||||||
|
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
|
||||||
|
RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6.0),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
"No",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 17,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
Expanded(
|
||||||
|
child: SizedBox(
|
||||||
|
height: (size.height / 50.52) * 2,
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ButtonStyle(
|
||||||
|
elevation: MaterialStateProperty.all(0),
|
||||||
|
backgroundColor:
|
||||||
|
MaterialStateProperty.all<Color>(Colors.white),
|
||||||
|
shape:
|
||||||
|
MaterialStateProperty.all<RoundedRectangleBorder>(
|
||||||
|
RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6.0),
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Colors.purple, width: 1))),
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.pushAndRemoveUntil(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => LoginScreen()),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
"Yes",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 17,
|
||||||
|
color: Colors.purple,
|
||||||
|
fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -7,12 +7,16 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
|
#include <flutter_secure_storage/flutter_secure_storage_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) flutter_secure_storage_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStoragePlugin");
|
||||||
|
flutter_secure_storage_plugin_register_with_registrar(flutter_secure_storage_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_linux
|
file_selector_linux
|
||||||
|
flutter_secure_storage
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
16
pubspec.lock
16
pubspec.lock
@ -342,6 +342,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.22"
|
version: "2.0.22"
|
||||||
|
flutter_secure_storage:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage
|
||||||
|
sha256: "9f3dd2ac3b6875b0fde5b04734789c3ef35ba3965c18e99dd564a7a2f8056df6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.1"
|
||||||
flutter_svg:
|
flutter_svg:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -664,6 +672,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
pretty_dio_logger:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: pretty_dio_logger
|
||||||
|
sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -54,6 +54,8 @@ dependencies:
|
|||||||
flutter_local_notifications: ^17.2.1+2
|
flutter_local_notifications: ^17.2.1+2
|
||||||
firebase_crashlytics: ^4.0.4
|
firebase_crashlytics: ^4.0.4
|
||||||
firebase_analytics: ^11.2.1
|
firebase_analytics: ^11.2.1
|
||||||
|
flutter_secure_storage: ^4.2.1
|
||||||
|
pretty_dio_logger: ^1.4.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
Reference in New Issue
Block a user