diff --git a/lib/academico_mobile.dart b/lib/academico_mobile.dart index 72975ef..1d21ce4 100644 --- a/lib/academico_mobile.dart +++ b/lib/academico_mobile.dart @@ -8,7 +8,7 @@ import 'package:academico_mobile/app/pages/recover_password/recover_password_pag import 'package:academico_mobile/app/pages/request_documents/request_documents_page.dart'; import 'package:academico_mobile/app/pages/schedule/schedule_router.dart'; import 'package:academico_mobile/app/pages/school_records/school_records_page.dart'; -import 'package:academico_mobile/app/pages/splash/splash_page.dart'; +import 'package:academico_mobile/app/pages/splash/splash_route.dart'; import 'package:flutter/material.dart'; class AcademicoMobile extends StatelessWidget { @@ -22,7 +22,7 @@ class AcademicoMobile extends StatelessWidget { theme: ThemeConfig.theme, debugShowCheckedModeBanner: false, routes: { - '/': (context) => const SplashPage(), + '/': (context) => SplashRoute.page, '/login': (context) => LoginRouter.page, '/schedule': (context) => ScheduleRouter.page, '/daily': (context) => DailyRouter.page, diff --git a/lib/app/core/secure_storage/secure_storage.dart b/lib/app/core/secure_storage/secure_storage.dart new file mode 100644 index 0000000..2a09b4e --- /dev/null +++ b/lib/app/core/secure_storage/secure_storage.dart @@ -0,0 +1,32 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class SecureStorage { + // Create storage + final storage = const FlutterSecureStorage( + aOptions: AndroidOptions( + encryptedSharedPreferences: true, + )); + + final String _keyUserName = 'username'; + final String _keyPassWord = 'password'; + + Future setUserName(String username) async { + await storage.write(key: _keyUserName, value: username); + } + + Future getUserName() async { + return await storage.read(key: _keyUserName); + } + + Future setPassWord(String password) async { + await storage.write(key: _keyPassWord, value: password); + } + + Future getPassWord() async { + return await storage.read(key: _keyPassWord); + } + + Future deleteAll() async { + await storage.deleteAll(); + } +} diff --git a/lib/app/pages/about/about_page.dart b/lib/app/pages/about/about_page.dart index a02b0c9..7c8a575 100644 --- a/lib/app/pages/about/about_page.dart +++ b/lib/app/pages/about/about_page.dart @@ -32,18 +32,20 @@ class AboutPage extends StatelessWidget { children: [ Image.asset( 'assets/images/logos/logo_academico.png', - height: context.percentHeight(.2), + width: context.percentWidth(.6), ), Text( 'Acadêmico Mobile', style: context.textStyles.texLabelH1.copyWith( color: context.colorsApp.cardwhite, - fontSize: context.percentHeight(.03), + fontSize: context.percentHeight(.025), ), ), + SizedBox(height: context.percentHeight(.01)), Text('Versão 1.0.0', style: context.textStyles.texLabelH4.copyWith( color: context.colorsApp.cardwhite, + fontSize: context.percentHeight(.015), )), ], ), diff --git a/lib/app/pages/home/home_controller.dart b/lib/app/pages/home/home_controller.dart index 9c9f092..51e5c46 100644 --- a/lib/app/pages/home/home_controller.dart +++ b/lib/app/pages/home/home_controller.dart @@ -29,7 +29,7 @@ class HomeController extends Cubit { emit(state.copyWith(status: HomeStateStatus.loading)); try { await Future.delayed(const Duration(seconds: 2)); - emit(state.copyWith(status: HomeStateStatus.loaded)); + emit(state.copyWith(status: HomeStateStatus.deslogado, isOn: false)); } catch (e, s) { log('Error ao realizar logout', error: e, stackTrace: s); emit(state.copyWith( diff --git a/lib/app/pages/home/home_page.dart b/lib/app/pages/home/home_page.dart index a25b05b..c00cd15 100644 --- a/lib/app/pages/home/home_page.dart +++ b/lib/app/pages/home/home_page.dart @@ -1,3 +1,4 @@ +import 'package:academico_mobile/app/core/secure_storage/secure_storage.dart'; import 'package:academico_mobile/app/core/ui/base_state/base_state.dart'; import 'package:academico_mobile/app/core/ui/helpers/size_extensions.dart'; import 'package:academico_mobile/app/core/ui/styles/colors_app.dart'; @@ -7,6 +8,7 @@ import 'package:academico_mobile/app/pages/home/home_state.dart'; import 'package:academico_mobile/app/pages/home/widgets/card_home.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -17,6 +19,8 @@ class HomePage extends StatefulWidget { class _HomePageState extends BaseState { final GlobalKey _scaffoldKey = GlobalKey(); + final SecureStorage _secureStorage = SecureStorage(); + final prefs = SharedPreferences.getInstance(); @override void onReady() { @@ -44,9 +48,8 @@ class _HomePageState extends BaseState { Widget finishDrawer(BuildContext context) => InkWell( onTap: () async { - await controller.logout().then((value) { - Navigator.of(context).pop(); - }); + prefs.then((value) => value.clear()); + await controller.logout(); }, child: Container( padding: EdgeInsets.only( @@ -174,6 +177,11 @@ class _HomePageState extends BaseState { hideLoader(); showError(state.errorMessage ?? 'Error ao carregar Home Page'); }, + deslogado: () { + hideLoader(); + _secureStorage.deleteAll(); + Navigator.of(context).pushReplacementNamed('/login'); + }, ); }, buildWhen: (previous, current) => current.status.matchAny( diff --git a/lib/app/pages/home/home_state.dart b/lib/app/pages/home/home_state.dart index 94d9e6f..1499d63 100644 --- a/lib/app/pages/home/home_state.dart +++ b/lib/app/pages/home/home_state.dart @@ -8,6 +8,7 @@ enum HomeStateStatus { initial, loading, loaded, + deslogado, error, } diff --git a/lib/app/pages/home/home_state.g.dart b/lib/app/pages/home/home_state.g.dart index 3911391..ba07876 100644 --- a/lib/app/pages/home/home_state.g.dart +++ b/lib/app/pages/home/home_state.g.dart @@ -11,6 +11,7 @@ extension HomeStateStatusMatch on HomeStateStatus { {required T Function() initial, required T Function() loading, required T Function() loaded, + required T Function() deslogado, required T Function() error}) { final v = this; if (v == HomeStateStatus.initial) { @@ -25,6 +26,10 @@ extension HomeStateStatusMatch on HomeStateStatus { return loaded(); } + if (v == HomeStateStatus.deslogado) { + return deslogado(); + } + if (v == HomeStateStatus.error) { return error(); } @@ -37,6 +42,7 @@ extension HomeStateStatusMatch on HomeStateStatus { T Function()? initial, T Function()? loading, T Function()? loaded, + T Function()? deslogado, T Function()? error}) { final v = this; if (v == HomeStateStatus.initial && initial != null) { @@ -51,6 +57,10 @@ extension HomeStateStatusMatch on HomeStateStatus { return loaded(); } + if (v == HomeStateStatus.deslogado && deslogado != null) { + return deslogado(); + } + if (v == HomeStateStatus.error && error != null) { return error(); } diff --git a/lib/app/pages/login/login_controller.dart b/lib/app/pages/login/login_controller.dart index 71e0cd6..f54ad06 100644 --- a/lib/app/pages/login/login_controller.dart +++ b/lib/app/pages/login/login_controller.dart @@ -14,8 +14,10 @@ class LoginController extends Cubit { try { emit(state.copyWith(status: LoginStatus.login)); final authModel = await _authRepository.login(matricula, password); + final prefs = await SharedPreferences.getInstance(); await prefs.setString('access_token', authModel.accessToken); + emit(state.copyWith(status: LoginStatus.sucess)); } on UnauthorizedException catch (e, s) { log('Permissão Negada Controller 1', error: e, stackTrace: s); diff --git a/lib/app/pages/login/login_page.dart b/lib/app/pages/login/login_page.dart index 809caa8..56b8863 100644 --- a/lib/app/pages/login/login_page.dart +++ b/lib/app/pages/login/login_page.dart @@ -1,4 +1,5 @@ // ignore_for_file: unnecessary_string_interpolations, avoid_print +import 'package:academico_mobile/app/core/secure_storage/secure_storage.dart'; import 'package:academico_mobile/app/core/ui/base_state/base_state.dart'; import 'package:academico_mobile/app/core/ui/helpers/size_extensions.dart'; import 'package:academico_mobile/app/core/ui/styles/colors_app.dart'; @@ -24,6 +25,19 @@ class _LoginPageState extends BaseState { TextEditingController passwordEC = TextEditingController(); bool _obscuredText = true; bool check = false; + final SecureStorage _secureStorage = SecureStorage(); + + @override + void initState() { + super.initState(); + fetchSecureStorageData(); + } + + Future fetchSecureStorageData() async { + matriculaEC.text = await _secureStorage.getUserName() ?? ''; + passwordEC.text = await _secureStorage.getPassWord() ?? ''; + print('username: ${matriculaEC.text}, password: ${passwordEC.text}'); + } @override void dispose() { @@ -32,14 +46,14 @@ class _LoginPageState extends BaseState { super.dispose(); } + void toogleCheck() { + setState(() { + check = !check; + }); + } + @override Widget build(BuildContext context) { - void toogleCheck() { - setState(() { - check = !check; - }); - } - return BlocListener( listener: (context, state) { state.status.matchAny( @@ -151,6 +165,16 @@ class _LoginPageState extends BaseState { formKey.currentState?.validate() ?? false; if (formValid) { + if (check == true) { + await _secureStorage + .setUserName(matriculaEC.text); + await _secureStorage + .setPassWord(passwordEC.text); + } else { + await _secureStorage.setUserName(''); + await _secureStorage.setPassWord(''); + } + await controller.login( matriculaEC.text, passwordEC.text, diff --git a/lib/app/pages/splash/splash_controller.dart b/lib/app/pages/splash/splash_controller.dart new file mode 100644 index 0000000..d2eddf0 --- /dev/null +++ b/lib/app/pages/splash/splash_controller.dart @@ -0,0 +1,30 @@ +import 'dart:developer'; +import 'package:academico_mobile/app/pages/splash/splash_state.dart'; +import 'package:academico_mobile/app/repositories/auth/auth_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SplashController extends Cubit { + final AuthRepository _authRepository; + SplashController(this._authRepository) : super(const SplashState.initial()); + + Future splashLogin(String matricula, String password) async { + emit(state.copyWith(status: SplashStatus.loading)); + try { + await Future.delayed(const Duration(seconds: 2)); + final authModel = await _authRepository.login(matricula, password); + + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('access_token', authModel.accessToken); + + emit(state.copyWith(status: SplashStatus.loaded)); + } catch (e, s) { + log('Erro ao realizar Login Controller 1', error: e, stackTrace: s); + emit( + state.copyWith( + status: SplashStatus.error, + errorMessage: 'Erro ao realizar Login Controller 2'), + ); + } + } +} diff --git a/lib/app/pages/splash/splash_page.dart b/lib/app/pages/splash/splash_page.dart index 26e7bc3..03b17a8 100644 --- a/lib/app/pages/splash/splash_page.dart +++ b/lib/app/pages/splash/splash_page.dart @@ -1,38 +1,82 @@ +import 'package:academico_mobile/app/core/secure_storage/secure_storage.dart'; +import 'package:academico_mobile/app/core/ui/base_state/base_state.dart'; import 'package:academico_mobile/app/core/ui/helpers/size_extensions.dart'; import 'package:academico_mobile/app/core/ui/widgets/my_input_button.dart'; +import 'package:academico_mobile/app/pages/splash/splash_controller.dart'; +import 'package:academico_mobile/app/pages/splash/splash_state.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; -class SplashPage extends StatelessWidget { +class SplashPage extends StatefulWidget { const SplashPage({super.key}); + @override + State createState() => _SplashPageState(); +} + +class _SplashPageState extends BaseState { + final SecureStorage _secureStorage = SecureStorage(); + late String username; + late String password; + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + username = await _secureStorage.getUserName() ?? ''; + password = await _secureStorage.getPassWord() ?? ''; + }); + } + @override Widget build(BuildContext context) { - return Scaffold( - body: Stack( - children: [ - Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'assets/images/logos/logo_ifce.png', - height: context.percentWidth(.5), - fit: BoxFit.cover, - ), - SizedBox( - height: context.percentHeight(.1), - ), - MyInputButton( - width: context.percentWidth(.6), - height: context.percentHeight(.07), - label: 'Acessar', - onPressed: () => - Navigator.of(context).popAndPushNamed('/login'), - ), - ], + return BlocListener( + listener: (context, state) { + state.status.matchAny( + any: () => hideLoader(), + loading: () => showLoader(), + loaded: () { + hideLoader(); + Navigator.of(context).pushReplacementNamed('/home'); + }, + error: () { + hideLoader(); + showError('Error ao realizar login'); + }, + ); + }, + child: Scaffold( + body: Stack( + children: [ + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/images/logos/logo_ifce.png', + height: context.percentWidth(.5), + fit: BoxFit.cover, + ), + SizedBox( + height: context.percentHeight(.1), + ), + MyInputButton( + width: context.percentWidth(.6), + height: context.percentHeight(.07), + label: 'Acessar', + onPressed: () async { + if (username.toString().isNotEmpty && + password.toString().isNotEmpty) { + controller.splashLogin(username, password); + } else { + Navigator.of(context).pushNamed('/login'); + } + }, + ), + ], + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/app/pages/splash/splash_route.dart b/lib/app/pages/splash/splash_route.dart new file mode 100644 index 0000000..16e49dd --- /dev/null +++ b/lib/app/pages/splash/splash_route.dart @@ -0,0 +1,18 @@ +import 'package:academico_mobile/app/pages/splash/splash_controller.dart'; +import 'package:academico_mobile/app/pages/splash/splash_page.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class SplashRoute { + SplashRoute._(); + static Widget get page => MultiProvider( + providers: [ + Provider( + create: (context) => SplashController( + context.read(), + ), + ), + ], + child: const SplashPage(), + ); +} diff --git a/lib/app/pages/splash/splash_state.dart b/lib/app/pages/splash/splash_state.dart new file mode 100644 index 0000000..faf6550 --- /dev/null +++ b/lib/app/pages/splash/splash_state.dart @@ -0,0 +1,39 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:equatable/equatable.dart'; +import 'package:match/match.dart'; + +part 'splash_state.g.dart'; + +@match +enum SplashStatus { + initial, + loading, + loaded, + error, +} + +class SplashState extends Equatable { + final SplashStatus status; + final String? errorMessage; + + const SplashState({ + required this.status, + this.errorMessage, + }); + const SplashState.initial() + : status = SplashStatus.initial, + errorMessage = null; + + @override + List get props => [status, errorMessage]; + + SplashState copyWith({ + SplashStatus? status, + String? errorMessage, + }) { + return SplashState( + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + ); + } +} diff --git a/lib/app/pages/splash/splash_state.g.dart b/lib/app/pages/splash/splash_state.g.dart new file mode 100644 index 0000000..aef7842 --- /dev/null +++ b/lib/app/pages/splash/splash_state.g.dart @@ -0,0 +1,60 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'splash_state.dart'; + +// ************************************************************************** +// MatchExtensionGenerator +// ************************************************************************** + +extension SplashStatusMatch on SplashStatus { + T match( + {required T Function() initial, + required T Function() loading, + required T Function() loaded, + required T Function() error}) { + final v = this; + if (v == SplashStatus.initial) { + return initial(); + } + + if (v == SplashStatus.loading) { + return loading(); + } + + if (v == SplashStatus.loaded) { + return loaded(); + } + + if (v == SplashStatus.error) { + return error(); + } + + throw Exception('SplashStatus.match failed, found no match for: $this'); + } + + T matchAny( + {required T Function() any, + T Function()? initial, + T Function()? loading, + T Function()? loaded, + T Function()? error}) { + final v = this; + if (v == SplashStatus.initial && initial != null) { + return initial(); + } + + if (v == SplashStatus.loading && loading != null) { + return loading(); + } + + if (v == SplashStatus.loaded && loaded != null) { + return loaded(); + } + + if (v == SplashStatus.error && error != null) { + return error(); + } + + return any(); + } +} diff --git a/pubspec.lock b/pubspec.lock index 040877b..19ae3d0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -262,6 +262,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "98352186ee7ad3639ccc77ad7924b773ff6883076ab952437d20f18a61f0a7c5" + url: "https://pub.dev" + source: hosted + version: "8.0.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "083add01847fc1c80a07a08e1ed6927e9acd9618a35e330239d4422cd2a58c50" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: b3773190e385a3c8a382007893d678ae95462b3c2279e987b55d140d3b0cb81b + url: "https://pub.dev" + source: hosted + version: "1.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "42938e70d4b872e856e678c423cc0e9065d7d294f45bc41fc1981a4eb4beaffe" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: fc2910ec9b28d60598216c29ea763b3a96c401f0ce1d13cdf69ccb0e5c93c3ee + url: "https://pub.dev" + source: hosted + version: "2.0.0" flutter_svg: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f800b7f..7af9c0b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: toggle_switch: ^2.0.1 validatorless: ^1.2.3 shared_preferences: ^2.1.0 + flutter_secure_storage: ^8.0.0 dev_dependencies: flutter_test: