diff --git a/lib/app_config.dart b/lib/app_config.dart index 74d2422b..e9c788aa 100644 --- a/lib/app_config.dart +++ b/lib/app_config.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'env.dart'; - -import 'routes/routes.dart'; -import 'app_theme.dart'; -import 'vaahextendflutter/tag/tag_panel.dart'; -import 'view/pages/home.dart'; +import 'package:team/app_theme.dart'; +import 'package:team/env.dart'; +import 'package:team/routes/middleware.dart'; +import 'package:team/vaahextendflutter/widgets/debug.dart'; final _navigatorKey = GlobalKey(); @@ -20,12 +18,9 @@ class AppConfig extends StatelessWidget { theme: ThemeData( primarySwatch: AppTheme.colors['primary'], ), - onGenerateInitialRoutes: (String initialRoute) { - return [TeamHomePage.route()]; - }, - onGenerateRoute: onGenerateRoute, + onGenerateRoute: routeMiddleware, builder: (BuildContext context, Widget? child) { - return TagPanelHost( + return DebugWidget( navigatorKey: _navigatorKey, child: child!, ); diff --git a/lib/app_theme.dart b/lib/app_theme.dart index d824953f..95f1c475 100644 --- a/lib/app_theme.dart +++ b/lib/app_theme.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; - -import 'vaahextendflutter/base/base_theme.dart'; +import 'package:team/vaahextendflutter/base/base_theme.dart'; class AppTheme { static final Map colors = Map.of(BaseTheme.colors); @@ -11,8 +10,6 @@ class AppTheme { } } - - // To define new color developer should visit https://colors.eva.design/ const MaterialColor pink = MaterialColor( diff --git a/lib/controllers/base_controller.dart b/lib/controllers/base_controller.dart index ec3b1b93..50e5eeae 100644 --- a/lib/controllers/base_controller.dart +++ b/lib/controllers/base_controller.dart @@ -1,24 +1,24 @@ import 'package:get/get.dart'; - -import '../env.dart'; -import '../app_theme.dart'; -import '../vaahextendflutter/helpers/console.dart'; -import '../vaahextendflutter/services/api.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:team/app_theme.dart'; +import 'package:team/controllers/root_assets_controller.dart'; +import 'package:team/env.dart'; +import 'package:team/vaahextendflutter/helpers/console.dart'; +import 'package:team/vaahextendflutter/services/api.dart'; class BaseController extends GetxController { Future init() async { - String environment = - const String.fromEnvironment('environment', defaultValue: 'default'); - final EnvController envController = Get.put( - EnvController( - environment, - ), - ); - Console.info('Env Type: ${envController.config.envType}'); - Console.info( - 'Version: ${envController.config.version}+${envController.config.build}', - ); - await Api.initApi(); + await GetStorage.init(); + initEnvController(); AppTheme.init(); + await Api.init(); + Get.put(RootAssetsController()); } } + +void initEnvController() { + String environment = const String.fromEnvironment('environment', defaultValue: 'default'); + final EnvController envController = Get.put(EnvController(environment)); + Console.info('Env Type: ${envController.config.envType}'); + Console.info('Version: ${envController.config.version}+${envController.config.build}'); +} diff --git a/lib/controllers/root_assets_controller.dart b/lib/controllers/root_assets_controller.dart new file mode 100644 index 00000000..77334a9f --- /dev/null +++ b/lib/controllers/root_assets_controller.dart @@ -0,0 +1,30 @@ +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; + +class RootAssetsController extends GetxController { + + RootAssetsController() { + if(storage.read('apiToken') != null){ + // get user by token User.byToken(); USE WEBHOOKS and SOCKETS + } + } + + final storage = GetStorage(); + + Map? _user; + Map? get user => _user; + set user(Map? user) { + _user = user; + update(); + } + + String? _apiToken; + String? get apiToken => _apiToken; + set apiToken(String? apiToken) { + storage.write('apiToken', apiToken); + _apiToken = apiToken; + update(); + } + + // TODO: Need to use api token in Api.ajax +} diff --git a/lib/env.dart b/lib/env.dart index 23c0c882..26474fe4 100644 --- a/lib/env.dart +++ b/lib/env.dart @@ -1,11 +1,9 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; - -import 'app_theme.dart'; -import 'vaahextendflutter/helpers/console.dart'; +import 'package:team/app_theme.dart'; +import 'package:team/vaahextendflutter/helpers/console.dart'; // After changing any const you will need to restart the app (Hot-reload won't work). diff --git a/lib/main.dart b/lib/main.dart index 796a91ff..f3e83704 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; - -import 'app_config.dart'; -import 'controllers/base_controller.dart'; +import 'package:team/app_config.dart'; +import 'package:team/controllers/base_controller.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/models/user.dart b/lib/models/user.dart new file mode 100644 index 00000000..5a596b57 --- /dev/null +++ b/lib/models/user.dart @@ -0,0 +1,113 @@ +import 'package:get/get.dart'; +import 'package:team/controllers/root_assets_controller.dart'; +import 'package:team/vaahextendflutter/services/api.dart'; + +class User { + static const String apiEndPoint = '/users'; // TODO: change end point + static final RootAssetsController rootAssetsController = Get.find(); + + static bool hasPermission(String value) { + if (rootAssetsController.user == null || rootAssetsController.user?['permissions'] == null) { + return false; + } + return (rootAssetsController.user?['permissions'] as List).contains(value); + } + + static Future signin(String identifier, String password) async { + Map user = await Api.ajax( + url: apiEndPoint, + method: 'post', + params: {"identifier": identifier, "password": password}, + ); + rootAssetsController.user = user; + rootAssetsController.apiToken = user['token']; + return; + } + + static Future forgotPassword(String identifier) async { + await Api.ajax( + url: apiEndPoint, // TODO: change end point + method: 'post', + params: {"identifier": identifier}, + ); + // TODO: On the same page of the call Show enter otp, reset pass + return; + } + + static Future signout() async { + rootAssetsController.user = null; + return; + } + + static Future createItem(Map item) async { + return await Api.ajax( + url: apiEndPoint, // TODO: change end point + method: 'post', + params: item, + ); + } + + static Future?> getList(Map query) async { + return await Api.ajax( + url: apiEndPoint, // TODO: change end point + method: 'get', + query: query, + ); + } + + static Future?> updateList(String type, List items) async { + return await Api.ajax( + url: apiEndPoint, // TODO: change end point + method: 'post', + params: {'type': type, 'data': items}, + ); + } + + static Future?> deleteList(String type, List items) async { + return await Api.ajax( + url: apiEndPoint, // TODO: change end point + method: 'delete', + params: {'type': type, 'data': items}, + ); + } + + static Future?> listAction(String type, List items) async { + return await Api.ajax( + url: apiEndPoint, // TODO: change end point + method: 'patch', + params: {'type': type, 'data': items}, + ); + } + + static Future getItem(String id) async { + return await Api.ajax( + url: apiEndPoint, // TODO: change end point + method: 'get', + query: {"id": id}, + ); + } + + static Future updateItem(String id, Map item) async { + return await Api.ajax( + url: apiEndPoint, // TODO: change end point + method: 'patch', + params: {'id': id, 'item': item}, + ); + } + + static Future deleteItem(String id) async { + return await Api.ajax( + url: apiEndPoint, // TODO: change end point + method: 'delete', + params: {"id": id}, + ); + } + + static Future itemAction(String id, String type) async { + return await Api.ajax( + url: apiEndPoint, // TODO: change end point + method: 'post', + params: {'id': id, 'type': type}, + ); + } +} diff --git a/lib/routes/middleware.dart b/lib/routes/middleware.dart new file mode 100644 index 00000000..b5f62f2f --- /dev/null +++ b/lib/routes/middleware.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; +import 'package:team/routes/routes.dart'; +import 'package:team/view/pages/not_found.dart'; + +Route? routeMiddleware(RouteSettings route) { + if (!routes.containsKey(route.name)) { + return NotFoundPage.route(); + } + return routes[route.name]!(); +} diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart index fd3f153e..918bf80f 100644 --- a/lib/routes/routes.dart +++ b/lib/routes/routes.dart @@ -1,14 +1,11 @@ import 'package:flutter/material.dart'; - -import '../view/pages/home.dart'; -import '../view/pages/something_went_wrong.dart'; - - -Map> routes = { - '/': TeamHomePage.route(), - SomethingWentWrong.routeName: SomethingWentWrong.route(), -}; - -Route? onGenerateRoute(RouteSettings route) { - return routes[route.name] ?? SomethingWentWrong.route(); -} +import 'package:team/view/pages/home.dart'; +import 'package:team/view/pages/not_found.dart'; +import 'package:team/view/pages/permission_denied.dart'; + +final Map Function()> routes = { + '/': HomePage.route, + HomePage.routePath: HomePage.route, + NotFoundPage.routePath: NotFoundPage.route, + PermissionDeniedPage.routePath: PermissionDeniedPage.route, +}; \ No newline at end of file diff --git a/lib/vaahextendflutter/helpers/constants.dart b/lib/vaahextendflutter/helpers/constants.dart index c785f268..baccc203 100644 --- a/lib/vaahextendflutter/helpers/constants.dart +++ b/lib/vaahextendflutter/helpers/constants.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -const double deafaultPadding = 16.0; -const double deafaultMargin = 16.0; +const double defaultPadding = 16.0; +const double defaultMargin = 16.0; // Common Widgets const emptyWidget = SizedBox(); diff --git a/lib/vaahextendflutter/helpers/helpers.dart b/lib/vaahextendflutter/helpers/helpers.dart index eefc12f9..7a9e7cba 100644 --- a/lib/vaahextendflutter/helpers/helpers.dart +++ b/lib/vaahextendflutter/helpers/helpers.dart @@ -34,7 +34,7 @@ class Helpers { AlertDialog( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all( - Radius.circular(deafaultPadding), + Radius.circular(defaultPadding), ), ), contentPadding: EdgeInsets.zero, diff --git a/lib/vaahextendflutter/services/api.dart b/lib/vaahextendflutter/services/api.dart index 50b99e21..6cfeda81 100644 --- a/lib/vaahextendflutter/services/api.dart +++ b/lib/vaahextendflutter/services/api.dart @@ -24,8 +24,7 @@ class Api { static final Dio _dio = Dio(); // Get request header options - static Future _getOptions( - {String contentType = Headers.jsonContentType}) async { + static Future _getOptions({String contentType = Headers.jsonContentType}) async { final Map header = {}; header.addAll({'Accept': 'application/json'}); header.addAll({'X-Requested-With': 'XMLHttpRequest'}); @@ -81,18 +80,16 @@ class Api { // return type of ajax is ApiResponseType? so if there is error // then null will be returned otherwise ApiResponseType object - static Future ajax({ + static Future ajax({ required String url, Future Function(dynamic data, Response? res)? callback, String method = 'get', Map? params, // eg: { 'name': 'abc' }. params is data passed in post, put, etc. requests. Map? query, // eg: { 'name': 'abc' } - List>? - headers, // eg: [{'title': 'content'}, {'key', 'value'}] + List>? headers, // eg: [{'title': 'content'}, {'key', 'value'}] int? customTimeoutLimit, - bool showAlert = - true, // if set false then on success or error, nothing will be shown + bool showAlert = true, // if set false then on success or error, nothing will be shown String alertType = 'toast', // 'toast' and 'dialog' are valid values Future Function()? onStart, Future Function()? onCompleted, @@ -137,7 +134,10 @@ class Api { ); } - return; + return { + 'data': _parseKeys(data: responseData, changeKeys: _snakeCasetoLowerCamelCase), + 'response': response + }; } catch (error) { // On completed, use for hide loading if (onCompleted != null) { @@ -168,7 +168,13 @@ class Api { if (callback != null) { await callback(null, null); } - return; + return { + 'data': null, + 'response': { + 'success': false, + 'errors': [error] + } + }; } // Here response error means server sends error response. eg 401: unauthorised @@ -181,7 +187,13 @@ class Api { if (callback != null) { await callback(null, null); } - return; + return { + 'data': null, + 'response': { + 'success': false, + 'errors': [error] + } + }; } if (callback != null) { @@ -196,7 +208,7 @@ class Api { } } - static Future initApi() async { + static Future init() async { bool envControllerExists = getx.Get.isRegistered(); if (!envControllerExists) { throw Exception('envController does not exist in app'); @@ -226,10 +238,8 @@ class Api { }) async { Response? response; final Options options = await _getOptions(); - options.sendTimeout = - customTimeoutLimit ?? _envController.config.timeoutLimit; - options.receiveTimeout = - customTimeoutLimit ?? _envController.config.timeoutLimit; + options.sendTimeout = customTimeoutLimit ?? _envController.config.timeoutLimit; + options.receiveTimeout = customTimeoutLimit ?? _envController.config.timeoutLimit; if (headers != null && headers.isNotEmpty) { if (options.headers != null) { for (Map element in headers) { @@ -302,8 +312,7 @@ class Api { await Helpers.showErrorDialog( title: 'Error', content: ['Invalid request type!'], - hint: - "get, post, put, patch, delete request types are allowed.", + hint: "get, post, put, patch, delete request types are allowed.", ); break; } @@ -337,8 +346,7 @@ class Api { ) async { if (response != null && response.data != null) { try { - final Map formatedResponse = - response.data as Map; + final Map formatedResponse = response.data as Map; dynamic responseData = formatedResponse['data']; if (responseData == null) { Console.warning('response doesn\'t contain data key.'); @@ -347,9 +355,8 @@ class Api { if (formatedResponse['messages'] == null) { Console.warning('response doesn\'t contain messages key.'); } else { - responseMessages = (formatedResponse['messages'] as List) - .map((e) => e.toString()) - .toList(); + responseMessages = + (formatedResponse['messages'] as List).map((e) => e.toString()).toList(); } String? responseHint = formatedResponse['hint'] as String?; if (responseHint == null) { @@ -474,13 +481,10 @@ class Api { if (error.response?.data != null) { try { Console.danger('${error.response}'); - final Map response = - error.response?.data as Map; + final Map response = error.response?.data as Map; Console.danger('$response'); if (response['errors'] != null) { - errors = (response['errors'] as List) - .map((e) => e.toString()) - .toList(); + errors = (response['errors'] as List).map((e) => e.toString()).toList(); } if (errors.isEmpty) { Console.warning('response doesn\'t contain errors key.'); @@ -520,8 +524,7 @@ class Api { // ignore: unnecessary_null_comparison if (Helpers.showErrorToast != null) { await Helpers.showErrorToast( - content: - errors.isEmpty ? 'Error' : 'ERR: ${errors.join('\n')}', + content: errors.isEmpty ? 'Error' : 'ERR: ${errors.join('\n')}', ); return; } @@ -569,8 +572,7 @@ class Api { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (content != null && content.isNotEmpty) - Text(content.join('\n')), + if (content != null && content.isNotEmpty) Text(content.join('\n')), if (content != null && content.isNotEmpty) verticalMargin12, if (hint != null && hint.trim().isNotEmpty) Text(hint), ], diff --git a/lib/vaahextendflutter/tag/tag_panel.dart b/lib/vaahextendflutter/widgets/debug.dart similarity index 95% rename from lib/vaahextendflutter/tag/tag_panel.dart rename to lib/vaahextendflutter/widgets/debug.dart index a8fd794e..c2f2e2ea 100644 --- a/lib/vaahextendflutter/tag/tag_panel.dart +++ b/lib/vaahextendflutter/widgets/debug.dart @@ -18,8 +18,8 @@ const double constHandleWidth = 180.0; // tag handle width const double constHandleHeight = 28.0; // tag handle height @immutable -class TagPanelHost extends StatefulWidget { - const TagPanelHost({ +class DebugWidget extends StatefulWidget { + const DebugWidget({ Key? key, required this.navigatorKey, required this.child, @@ -29,14 +29,14 @@ class TagPanelHost extends StatefulWidget { final Widget child; @override - TagPanelHostState createState() => TagPanelHostState(); + DebugWidgetState createState() => DebugWidgetState(); - static TagPanelHostState of(BuildContext context) { - return context.findAncestorStateOfType()!; + static DebugWidgetState of(BuildContext context) { + return context.findAncestorStateOfType()!; } } -class TagPanelHostState extends State +class DebugWidgetState extends State with SingleTickerProviderStateMixin { final _drawerKey = GlobalKey(); final _focusScopeNode = FocusScopeNode(); @@ -160,11 +160,11 @@ class TagPanelHostState extends State padding: EdgeInsets.only( top: MediaQuery.of(context).padding.top + - deafaultPadding + + defaultPadding + _handleHeight, - bottom: deafaultPadding, - left: deafaultPadding, - right: deafaultPadding, + bottom: defaultPadding, + left: defaultPadding, + right: defaultPadding, ), children: [ SelectableText( diff --git a/lib/view/pages/home.dart b/lib/view/pages/home.dart index 82885356..d5d561d5 100644 --- a/lib/view/pages/home.dart +++ b/lib/view/pages/home.dart @@ -1,30 +1,29 @@ import 'package:flutter/material.dart'; +import 'package:team/vaahextendflutter/base/base_stateful.dart'; -import '../../vaahextendflutter/base/base_stateful.dart'; +class HomePage extends StatefulWidget { + static const String routePath = '/home'; -class TeamHomePage extends StatefulWidget { static Route route() { return MaterialPageRoute( - settings: const RouteSettings(name: '/'), - builder: (_) => const TeamHomePage(), + settings: const RouteSettings(name: routePath), + builder: (_) => const HomePage(), ); } - const TeamHomePage({super.key}); + const HomePage({super.key}); @override - State createState() => _TeamHomePageState(); + State createState() => _HomePageState(); } -class _TeamHomePageState extends BaseStateful { +class _HomePageState extends BaseStateful { @override Widget build(BuildContext context) { super.build(context); return Scaffold( appBar: AppBar(), - body: const Center( - child: Text('Team App Home Page'), - ), + body: const Center(child: Text('WebReinvent')), ); } } diff --git a/lib/view/pages/not_found.dart b/lib/view/pages/not_found.dart new file mode 100644 index 00000000..04581edb --- /dev/null +++ b/lib/view/pages/not_found.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:team/vaahextendflutter/base/base_stateless.dart'; + +class NotFoundPage extends BaseStateless { + static const String routePath = '/page-not-found'; + + static Route route() { + return MaterialPageRoute( + settings: const RouteSettings(name: routePath), + builder: (_) => const NotFoundPage(), + ); + } + + const NotFoundPage({super.key}); + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + appBar: AppBar(), + body: const Center( + child: Text('Page Not Found!'), + ), + ); + } +} diff --git a/lib/view/pages/permission_denied.dart b/lib/view/pages/permission_denied.dart new file mode 100644 index 00000000..58374b3c --- /dev/null +++ b/lib/view/pages/permission_denied.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:team/vaahextendflutter/base/base_stateless.dart'; + +class PermissionDeniedPage extends BaseStateless { + static const String routePath = '/permission-denied'; + + static Route route() { + return MaterialPageRoute( + settings: const RouteSettings(name: routePath), + builder: (_) => const PermissionDeniedPage(), + ); + } + + const PermissionDeniedPage({super.key}); + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + appBar: AppBar(), + body: const Center( + child: Text('Permission Denied'), + ), + ); + } +} diff --git a/lib/view/pages/something_went_wrong.dart b/lib/view/pages/something_went_wrong.dart deleted file mode 100644 index 8d85bb25..00000000 --- a/lib/view/pages/something_went_wrong.dart +++ /dev/null @@ -1,39 +0,0 @@ - -import 'package:flutter/material.dart'; - -import '../../vaahextendflutter/base/base_stateful.dart'; - -class SomethingWentWrong extends StatefulWidget { - static String routeName = '/something-went-wrong'; - - static Route route() { - return MaterialPageRoute( - settings: const RouteSettings(name: '/something-went-wrong'), - builder: (_) => const SomethingWentWrong(), - ); - } - - const SomethingWentWrong({super.key}); - - @override - State createState() => _SomethingWentWrongState(); -} - -class _SomethingWentWrongState extends BaseStateful { - - @override - void afterFirstBuild(BuildContext context) { - super.afterFirstBuild(context); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return Scaffold( - appBar: AppBar(), - body: const Center( - child: Text('Something Went Wrong'), - ), - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index e658acc1..8d2a0a03 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -64,6 +64,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.4" flutter: dependency: "direct main" description: flutter @@ -107,6 +121,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.6.5" + get_storage: + dependency: "direct main" + description: + name: get_storage + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" http_parser: dependency: transitive description: @@ -156,6 +177,76 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.2" + path_provider: + dependency: transitive + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.21" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.7" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" sky_engine: dependency: transitive description: flutter @@ -217,6 +308,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+2" sdks: dart: ">=2.18.2 <3.0.0" flutter: ">=3.3.4" diff --git a/pubspec.yaml b/pubspec.yaml index 814c6eb8..6e8622bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: flutter_screenutil: ^5.5.4 dio: ^4.0.6 fluttertoast: ^8.1.1 + get_storage: ^2.0.3 dev_dependencies: flutter_test: