From 5f0e46a1b8763c72a72b636752023ab42ac9a700 Mon Sep 17 00:00:00 2001 From: itsscb Date: Fri, 3 Nov 2023 15:35:44 +0100 Subject: [PATCH 1/5] ft/adds backend service TODO: everything new --- frontend/app/lib/data/database.dart | 53 ++ frontend/app/lib/main.dart | 4 +- frontend/app/lib/model/apis/api_response.dart | 17 + .../app/lib/model/apis/app_exception.dart | 32 + .../lib/model/services/backend_service.dart | 237 +++++++ .../app/lib/model/view_model/account_vm.dart | 56 ++ frontend/app/lib/pages/dashboard_page.dart | 332 +++++----- frontend/app/lib/pages/login_page.dart | 85 +-- frontend/app/lib/pages/register_page.dart | 144 +---- frontend/app/lib/pages/start_page.dart | 587 +++++++++--------- frontend/app/pubspec.lock | 16 + frontend/app/pubspec.yaml | 1 + 12 files changed, 910 insertions(+), 654 deletions(-) create mode 100644 frontend/app/lib/model/apis/api_response.dart create mode 100644 frontend/app/lib/model/apis/app_exception.dart create mode 100644 frontend/app/lib/model/services/backend_service.dart create mode 100644 frontend/app/lib/model/view_model/account_vm.dart diff --git a/frontend/app/lib/data/database.dart b/frontend/app/lib/data/database.dart index 9cf0335..7d7e411 100644 --- a/frontend/app/lib/data/database.dart +++ b/frontend/app/lib/data/database.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:app/pb/session.pbjson.dart'; import 'package:fixnum/fixnum.dart'; import 'package:app/pb/google/protobuf/timestamp.pb.dart'; @@ -40,6 +41,12 @@ class Session { ); Session s = Session(); s._database = db; + final sessions = await s.getSessions(); + if (sessions.isNotEmpty) { + final session = sessions[0]; + session._database = db; + return session; + } return s; } @@ -90,6 +97,41 @@ class Session { return 'Session{accountId: $accountId, sessionId: $sessionId, accessToken: $accessToken, accessTokenExpiresAt: ${accessTokenExpiresAt.toString()}, refreshToken: $refreshToken, refreshTokenExpiresAt: ${refreshTokenExpiresAt.toString()}}'; } + static newSession(Session session) async { + final db = await openDatabase( + join(await getDatabasesPath(), 'df_database.db'), + onCreate: (db, version) { + return db.execute( + 'CREATE TABLE sessions(accountId INTEGER PRIMARY KEY, sessionId TEXT, accessToken TEXT, accessTokenExpiresAt TEXT, refreshToken TEXT, refreshTokenExpiresAt TEXT)', + ); + }, + version: 1, + ); + + await db.insert( + 'sessions', + session.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + static Future updateToken(Session s) async { + final db = await openDatabase( + join(await getDatabasesPath(), 'df_database.db'), + onCreate: (db, version) { + return db.execute( + 'CREATE TABLE sessions(accountId INTEGER PRIMARY KEY, sessionId TEXT, accessToken TEXT, accessTokenExpiresAt TEXT, refreshToken TEXT, refreshTokenExpiresAt TEXT)', + ); + }, + version: 1, + ); + await db.update( + 'sessions', + s.toMap(), + ); + return s; //await getSession(s.accountId!); + } + Future insertSession(Session session) async { // print('INSERTING SESSION: ${session.sessionId}'); final db = _database; @@ -101,6 +143,17 @@ class Session { // print('INSERT RESULT: $result'); } + Future updateSession(Session session) async { + sessionId = session.sessionId; + accessToken = session.accessToken; + accessTokenExpiresAt = session.accessTokenExpiresAt; + refreshToken = session.refreshToken; + refreshTokenExpiresAt = session.refreshTokenExpiresAt; + final db = _database; + await db.update('sessions', session.toMap()); + return session; + } + Future removeSession(String sessionId) async { final db = _database; await db.delete('sessions', where: 'sessionId = ?', whereArgs: [sessionId]); diff --git a/frontend/app/lib/main.dart b/frontend/app/lib/main.dart index 8e44a71..e790121 100644 --- a/frontend/app/lib/main.dart +++ b/frontend/app/lib/main.dart @@ -45,8 +45,8 @@ void main() async { )), home: Background( child: StartPage( - client: await GClient.client, - ), + // client: await GClient.client, + ), ), ), ); diff --git a/frontend/app/lib/model/apis/api_response.dart b/frontend/app/lib/model/apis/api_response.dart new file mode 100644 index 0000000..764c500 --- /dev/null +++ b/frontend/app/lib/model/apis/api_response.dart @@ -0,0 +1,17 @@ +class ApiResponse { + Status status; + T? data; + String? message; + + ApiResponse.initial(this.message) : status = Status.INITIAL; + ApiResponse.loading(this.message) : status = Status.LOADING; + ApiResponse.completed(this.data, this.message) : status = Status.COMPLETED; + ApiResponse.error(this.message) : status = Status.ERROR; + + @override + String toString() { + return "Status : $status \n Message: $message \n Data : $data"; + } +} + +enum Status { INITIAL, LOADING, COMPLETED, ERROR } diff --git a/frontend/app/lib/model/apis/app_exception.dart b/frontend/app/lib/model/apis/app_exception.dart new file mode 100644 index 0000000..7093126 --- /dev/null +++ b/frontend/app/lib/model/apis/app_exception.dart @@ -0,0 +1,32 @@ +class AppException implements Exception { + final _message; + final _prefix; + + AppException([this._message, this._prefix]); + + String toString() { + return "$_prefix$_message"; + } +} + +class FetchDataException extends AppException { + FetchDataException([String? message]) + : super(message, 'Fehler bei der Kommunikation: '); +} + +class BadRequestException extends AppException { + BadRequestException([message]) : super(message, 'Ungültige Anfrage; '); +} + +class UnauthorizedException extends AppException { + UnauthorizedException([message]) + : super(message, 'Nicht authorisierte Anfrage; '); +} + +class InvalidInputException extends AppException { + InvalidInputException([message]) : super(message, 'Ungültige Eingabe; '); +} + +class InternalException extends AppException { + InternalException([message]) : super(message, 'Interner Fehler; '); +} diff --git a/frontend/app/lib/model/services/backend_service.dart b/frontend/app/lib/model/services/backend_service.dart new file mode 100644 index 0000000..6cc42d0 --- /dev/null +++ b/frontend/app/lib/model/services/backend_service.dart @@ -0,0 +1,237 @@ +import 'dart:io'; + +import 'package:app/model/apis/app_exception.dart'; +import 'package:app/pb/account.pb.dart'; +import 'package:app/pb/account_info.pb.dart'; +import 'package:app/pb/person.pb.dart'; +import 'package:app/data/database.dart'; +import 'package:app/pb/rpc_get_account.pb.dart'; +import 'package:app/pb/rpc_get_account_info.pb.dart'; +import 'package:app/pb/rpc_get_person.pb.dart'; +import 'package:app/pb/rpc_login.pb.dart'; +import 'package:app/pb/rpc_refresh_token.pb.dart'; +import 'package:app/pb/service_df.pbgrpc.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:grpc/grpc.dart'; +import 'package:sqflite/sqflite.dart'; + +class BackendService { + BackendService() { + _init(); + } + final String baseUrl = '10.0.0.2'; + final String port = '9090'; + + late Session _session; + + final dfClient _client = dfClient( + ClientChannel('10.0.2.2', + port: 9090, + options: const ChannelOptions( + credentials: ChannelCredentials.insecure(), + )), + options: CallOptions( + timeout: const Duration(seconds: 5), + ), + ); + + static get client => dfClient( + ClientChannel('10.0.2.2', + port: 9090, + options: const ChannelOptions( + credentials: ChannelCredentials.insecure(), + )), + options: CallOptions( + timeout: const Duration(seconds: 5), + ), + ); + + void _init() { + Session.session.then((value) => _session = value); + } + + Future _isLoggedIn() async { + Session session = await Session.session; + if (session.accessToken == null || + session.refreshToken == null || + session.accountId == null) { + return null; + } + + if (session.accessTokenExpiresAt == null) { + return null; + } + + if (session.refreshTokenExpiresAt == null) { + return null; + } + + if (session.refreshTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) { + return null; + } else { + if (session.refreshToken != null && + session.accessTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) { + session = await refreshToken(session); + } + } + + return session; + } + + static Future get isLoggedIn async { + Session session = await Session.session; + if (session.accessToken == null || + session.refreshToken == null || + session.accountId == null) { + return false; + } + + if (session.accessTokenExpiresAt == null) { + return false; + } + + if (session.refreshTokenExpiresAt == null) { + return false; + } + + if (session.refreshTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) { + return false; + } + + return true; + } + + Future getAccount() async { + Session? session = await _isLoggedIn(); + if (session == null) { + throw UnauthorizedException('Sitzung ist abgelaufen'); + } + try { + final GetAccountResponse response = await _client.getAccount( + GetAccountRequest(id: session!.accountId), + options: CallOptions( + metadata: {'Authorization': 'Bearer ${session.accessToken}'})); + return response.account; + } on SocketException { + throw FetchDataException('Keine Internet Verbindung'); + } on GrpcError catch (err) { + // if (err.code == 16) { + // await refreshToken(session); + // return getAccount(); + // } + throw FetchDataException(err.message); + } catch (err) { + throw InternalException(err.toString()); + } + } + + Future getAccountInfo() async { + Session session = await Session.session; + if (session.accessTokenExpiresAt == null) { + throw UnauthorizedException('Keine Siztung gefunden'); + } + if (session.accessTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) { + session = await refreshToken(session); + if (session.accessTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) { + throw UnauthorizedException('Sitzung ist abgelaufen'); + } + } + try { + final GetAccountInfoResponse response = await _client + .getAccountInfo(GetAccountInfoRequest(accountId: _session.accountId)); + return response.accountInfo; + } on SocketException { + throw FetchDataException('Keine Internet Verbindung'); + } on GrpcError catch (err) { + if (err.code == 16) { + await refreshToken(session); + return getAccountInfo(); + } + throw FetchDataException(err.message); + } catch (err) { + throw InternalException(err.toString()); + } + } + + Future getPerson(Int64 personId) async { + Session session = await Session.session; + if (session.accessTokenExpiresAt == null) { + throw UnauthorizedException('Keine Siztung gefunden'); + } + if (session.accessTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) { + session = await refreshToken(session); + if (session.accessTokenExpiresAt == null) { + throw UnauthorizedException('Sitzung ist abgelaufen'); + } + } + try { + final GetPersonResponse response = + await _client.getPerson(GetPersonRequest(id: personId)); + return response.person; + } on SocketException { + throw FetchDataException('Keine Internet Verbindung'); + } on GrpcError catch (err) { + throw FetchDataException(err.message); + } catch (err) { + throw InternalException(err.toString()); + } + } + + // Future> listPersons() async { + // if (_session.accessToken == null) { + // refreshToken(); + // } + // try { + // ListPersonsResponse response = + // await _client.listPersons(ListPersonsRequest(accountId: _session.accountId)); + // return response.persons; + // } on SocketException { + // throw FetchDataException('Keine Internet Verbindung'); + // } on GrpcError catch (err) { + // throw FetchDataException(err.message); + // } catch (err) { + // throw InternalException(err.toString()); + // } + // } + static Future login( + {required String email, required String password}) async { + try { + final LoginResponse response = await BackendService.client.login( + LoginRequest( + email: email, + password: password, + ), + ); + await Session.newSession(response as Session); + return response.accessToken != ''; + } on SocketException { + throw FetchDataException('Keine Internet Verbindung'); + } on GrpcError catch (err) { + throw FetchDataException(err.message); + } catch (err) { + throw InternalException(err.toString()); + } + } + + Future refreshToken(Session session) async { + try { + final RefreshTokenResponse response = await _client.refreshToken( + RefreshTokenRequest(refreshToken: session.refreshToken)); + session.accessToken = response.accessToken; + session.accessTokenExpiresAt = response.accessTokenExpiresAt; + session = await Session.updateToken(session); + return session; + } on SocketException { + throw FetchDataException('Keine Internet Verbindung'); + } on GrpcError catch (err) { + throw FetchDataException(err.message); + } catch (err) { + throw InternalException(err.toString()); + } + } + + static Future logout() async { + Session session = await Session.session; + session.reset(); + } +} diff --git a/frontend/app/lib/model/view_model/account_vm.dart b/frontend/app/lib/model/view_model/account_vm.dart new file mode 100644 index 0000000..13356fa --- /dev/null +++ b/frontend/app/lib/model/view_model/account_vm.dart @@ -0,0 +1,56 @@ +import 'package:app/model/apis/api_response.dart'; +import 'package:app/model/services/backend_service.dart'; +import 'package:app/pb/account.pb.dart'; +import 'package:flutter/material.dart'; + +class AccountViewModel with ChangeNotifier { + AccountViewModel() { + _init(); + } + ApiResponse _apiResponse = ApiResponse.initial('Keine Daten'); + + final BackendService _service = BackendService(); + Account? _account; + + ApiResponse get response { + return _apiResponse; + } + + Account? get account { + return _account; + } + + void _init() async { + _apiResponse = ApiResponse.loading('Bereite alles vor'); + try { + _apiResponse = ApiResponse.completed(await _service.getAccount(), 'done'); + } catch (e) { + _apiResponse = ApiResponse.error(e.toString()); + } + notifyListeners(); + } + + Future fetchAccount() async { + _apiResponse = ApiResponse.loading('Hole Account'); + notifyListeners(); + try { + _apiResponse = ApiResponse.completed(await _service.getAccount(), 'done'); + } catch (e) { + _apiResponse = ApiResponse.error(e.toString()); + } + notifyListeners(); + } + + Future logout() async { + _apiResponse = ApiResponse.loading('Logge aus'); + notifyListeners(); + try { + await BackendService.logout(); + _apiResponse = ApiResponse.completed(null, 'Erfolgreich ausgeloggt'); + } catch (e) { + _apiResponse = ApiResponse.error(e.toString()); + } + print(_apiResponse.message); + notifyListeners(); + } +} diff --git a/frontend/app/lib/pages/dashboard_page.dart b/frontend/app/lib/pages/dashboard_page.dart index 6be86ba..14d3a25 100644 --- a/frontend/app/lib/pages/dashboard_page.dart +++ b/frontend/app/lib/pages/dashboard_page.dart @@ -11,10 +11,10 @@ import 'package:flutter/material.dart'; class DashboardPage extends StatefulWidget { const DashboardPage({ super.key, - required this.client, + // required this.client, }); - final GClient client; + // final GClient client; @override State createState() => _DashboardPageState(); @@ -35,44 +35,44 @@ class _DashboardPageState extends State { super.initState(); _setLoading(true); - widget.client.getAccountInfo( - GetAccountInfoRequest( - accountId: widget.client.session.accountId, - ), - onError: ({String? msg}) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Text('AccountInfo konnte nicht geladen werden'), - action: msg != null - ? SnackBarAction( - label: 'Details', - textColor: Colors.grey, - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - content: Text( - msg, - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.black), - ), - icon: const Icon( - Icons.warning, - color: Colors.red, - ), - ); - }, - ); - }) - : null, - ), - ); - }, - ).then((value) { - accountInfo = value.accountInfo; - _setLoading(false); - }); + // widget.client.getAccountInfo( + // GetAccountInfoRequest( + // accountId: widget.client.session.accountId, + // ), + // onError: ({String? msg}) { + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar( + // content: const Text('AccountInfo konnte nicht geladen werden'), + // action: msg != null + // ? SnackBarAction( + // label: 'Details', + // textColor: Colors.grey, + // onPressed: () { + // showDialog( + // context: context, + // builder: (context) { + // return AlertDialog( + // content: Text( + // msg, + // textAlign: TextAlign.center, + // style: const TextStyle(color: Colors.black), + // ), + // icon: const Icon( + // Icons.warning, + // color: Colors.red, + // ), + // ); + // }, + // ); + // }) + // : null, + // ), + // ); + // }, + // ).then((value) { + // accountInfo = value.accountInfo; + // _setLoading(false); + // }); } @override @@ -144,139 +144,139 @@ class _DashboardPageState extends State { ], ), ), - if (widget.client.session.accessToken != null) - TextButton( - onPressed: () { - widget.client.session.accessToken = null; - widget.client.session - .removeSession(widget.client.session.sessionId!); + // if (widget.client.session.accessToken != null) + // TextButton( + // onPressed: () { + // widget.client.session.accessToken = null; + // widget.client.session + // .removeSession(widget.client.session.sessionId!); - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (context) => - StartPage(client: widget.client), - ), - (route) => false); - }, - child: const Row( - children: [ - Text( - 'Log out', - style: TextStyle(fontSize: 20), - ), - Spacer(), - Icon( - Icons.logout, - color: Colors.white, - ), - ], - ), - ), + // Navigator.of(context).pushAndRemoveUntil( + // MaterialPageRoute( + // builder: (context) => + // StartPage(client: widget.client), + // ), + // (route) => false); + // }, + // child: const Row( + // children: [ + // Text( + // 'Log out', + // style: TextStyle(fontSize: 20), + // ), + // Spacer(), + // Icon( + // Icons.logout, + // color: Colors.white, + // ), + // ], + // ), + // ), const SizedBox( height: 250, ) ], ); }), - bottomNavigationBar: Builder( - builder: (context) { - return BottomBar( - children: widget.client.session.accessToken != null - ? [ - BottomNavigationBarItem( - backgroundColor: Colors.white, - label: 'Personen', - icon: Column( - children: [ - IconButton( - onPressed: () => - Scaffold.of(context).openDrawer(), - icon: const Icon( - Icons.group, - color: Colors.white, - ), - ), - const Text( - 'Personen', - style: TextStyle( - color: Colors.white, - fontSize: 16, - ), - ) - ], - ), - ), - BottomNavigationBarItem( - backgroundColor: Colors.white, - label: 'Home', - icon: Column( - children: [ - IconButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => StartPage( - client: widget.client, - ), - ), - ); - }, - icon: const Icon( - Icons.home, - color: Colors.white, - ), - ), - const Text( - 'Home', - style: TextStyle( - color: Colors.white, - fontSize: 16, - ), - ) - ], - ), - ), - BottomNavigationBarItem( - backgroundColor: Colors.white, - label: 'Menu', - icon: IconButton( - onPressed: () { - Scaffold.of(context).openDrawer(); - }, - icon: const Icon( - Icons.menu, - color: Colors.white, - ), - ), - ) - ] - : [ - BottomNavigationBarItem( - label: 'back', - backgroundColor: Colors.white, - icon: IconButton( - onPressed: () {}, - icon: const Icon( - Icons.arrow_back, - color: Colors.white, - ), - ), - ), - BottomNavigationBarItem( - backgroundColor: Colors.white, - label: 'Menu', - icon: IconButton( - onPressed: () => Scaffold.of(context).openDrawer(), - icon: const Icon( - Icons.menu, - color: Colors.white, - ), - ), - ), - ], - ); - }, - ), + // bottomNavigationBar: Builder( + // builder: (context) { + // return BottomBar( + // children: widget.client.session.accessToken != null + // ? [ + // BottomNavigationBarItem( + // backgroundColor: Colors.white, + // label: 'Personen', + // icon: Column( + // children: [ + // IconButton( + // onPressed: () => + // Scaffold.of(context).openDrawer(), + // icon: const Icon( + // Icons.group, + // color: Colors.white, + // ), + // ), + // const Text( + // 'Personen', + // style: TextStyle( + // color: Colors.white, + // fontSize: 16, + // ), + // ) + // ], + // ), + // ), + // BottomNavigationBarItem( + // backgroundColor: Colors.white, + // label: 'Home', + // icon: Column( + // children: [ + // IconButton( + // onPressed: () { + // Navigator.of(context).push( + // MaterialPageRoute( + // builder: (context) => StartPage( + // client: widget.client, + // ), + // ), + // ); + // }, + // icon: const Icon( + // Icons.home, + // color: Colors.white, + // ), + // ), + // const Text( + // 'Home', + // style: TextStyle( + // color: Colors.white, + // fontSize: 16, + // ), + // ) + // ], + // ), + // ), + // BottomNavigationBarItem( + // backgroundColor: Colors.white, + // label: 'Menu', + // icon: IconButton( + // onPressed: () { + // Scaffold.of(context).openDrawer(); + // }, + // icon: const Icon( + // Icons.menu, + // color: Colors.white, + // ), + // ), + // ) + // ] + // : [ + // BottomNavigationBarItem( + // label: 'back', + // backgroundColor: Colors.white, + // icon: IconButton( + // onPressed: () {}, + // icon: const Icon( + // Icons.arrow_back, + // color: Colors.white, + // ), + // ), + // ), + // BottomNavigationBarItem( + // backgroundColor: Colors.white, + // label: 'Menu', + // icon: IconButton( + // onPressed: () => Scaffold.of(context).openDrawer(), + // icon: const Icon( + // Icons.menu, + // color: Colors.white, + // ), + // ), + // ), + // ], + // ); + // }, + // ), body: !_loading ? Background( child: Center( diff --git a/frontend/app/lib/pages/login_page.dart b/frontend/app/lib/pages/login_page.dart index daf5e9b..b7d49d2 100644 --- a/frontend/app/lib/pages/login_page.dart +++ b/frontend/app/lib/pages/login_page.dart @@ -1,4 +1,5 @@ import 'package:app/gapi/client.dart'; +import 'package:app/model/services/backend_service.dart'; import 'package:app/pages/start_page.dart'; import 'package:app/widgets/background.dart'; import 'package:app/widgets/bottom_bar.dart'; @@ -12,11 +13,11 @@ import 'package:grpc/grpc.dart'; class LoginPage extends StatefulWidget { const LoginPage({ super.key, - required this.client, + // required this.client, // required this.onChangePage, }); - final GClient client; + // final GClient client; // void Function(Pages page) onChangePage; @override @@ -87,7 +88,7 @@ class _LoginPageState extends State { label: 'back', backgroundColor: Colors.white, icon: IconButton( - onPressed: () => Navigator.of(context).pop(widget.client), + onPressed: () => Navigator.of(context).pop(), icon: const Icon( Icons.arrow_back, color: Colors.white, @@ -250,73 +251,23 @@ class _LoginPageState extends State { if (_formKey.currentState!.validate()) { // final navigator = Navigator.of(context); _setLoading(true); - widget.client - .login( + BackendService.login( email: mailController.text, password: passwordController.text, - onError: ({GrpcError? error}) { - _setLoading(false); - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar( - content: const Text( - 'Login fehlgeschlagen', - ), - action: SnackBarAction( - textColor: Colors.grey, - label: 'Details', - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - content: error != null - ? Text( - 'Fehler: ${error.message}', - textAlign: - TextAlign.center, - style: - const TextStyle( - color: Colors - .black), - ) - : const Text( - 'Interner Fehler', - textAlign: - TextAlign.center, - style: TextStyle( - color: - Colors.black), - ), - icon: const Icon( - Icons.error, - color: Colors.black, - ), - ); - }, - ); - }), - )); - }, - onSuccess: () { - // _setLoading(false); - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar( - content: Text('Login erfolgreich'), - )); - }, - ) - .then( + ).then( (r) { - if (r.accessToken != '') { - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute( - builder: (ctx) => StartPage( - client: widget.client, - ), - ), - (ctx) => false, - ); + if (r) { + Navigator.pop(context); + Navigator.pop(context); + // Navigator.pushAndRemoveUntil( + // context, + // MaterialPageRoute( + // builder: (ctx) => const StartPage( + // // client: widget.client, + // ), + // ), + // (ctx) => false, + // ); // widget.onChangePage( // Pages.dashboard, // ); diff --git a/frontend/app/lib/pages/register_page.dart b/frontend/app/lib/pages/register_page.dart index 1257b00..d5ee106 100644 --- a/frontend/app/lib/pages/register_page.dart +++ b/frontend/app/lib/pages/register_page.dart @@ -10,10 +10,10 @@ import 'package:grpc/grpc.dart'; class RegisterPage extends StatefulWidget { const RegisterPage({ super.key, - required this.client, + // required this.client, }); - final GClient client; + // final GClient client; @override State createState() => _RegisterPageState(); @@ -112,7 +112,7 @@ class _RegisterPageState extends State { label: 'back', backgroundColor: Colors.white, icon: IconButton( - onPressed: () => Navigator.of(context).pop(widget.client), + onPressed: () => Navigator.of(context).pop(), icon: const Icon( Icons.arrow_back, color: Colors.white, @@ -209,143 +209,7 @@ class _RegisterPageState extends State { ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { - _setLoading(true); - widget.client - .createAccount( - email: mailController.text, - password: passwordController.text, - onError: ({GrpcError? error}) { - _setLoading(false); - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar( - content: const Text( - 'Login fehlgeschlagen', - ), - action: SnackBarAction( - textColor: Colors.grey, - label: 'Details', - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - content: error != null - ? Text( - 'Fehler: ${error.message}', - textAlign: - TextAlign - .center, - style: const TextStyle( - color: Colors - .black), - ) - : const Text( - 'Interner Fehler', - textAlign: - TextAlign - .center, - style: TextStyle( - color: Colors - .black), - ), - icon: const Icon( - Icons.error, - color: Colors.black, - ), - ); - }, - ); - }), - )); - }) - .then( - (r) { - if (r.account.secretKey != '') { - widget.client - .login( - email: mailController.text, - password: - passwordController.text, - onError: ( - {GrpcError? error}) {}, - onSuccess: () {}) - .then((resp) { - widget.client.getAccount( - accountId: r.account.id, - onError: ({GrpcError? err}) { - _setLoading(false); - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: const Text( - 'Login fehlgeschlagen', - ), - action: SnackBarAction( - textColor: Colors.grey, - label: 'Details', - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - content: err != - null - ? Text( - 'Hoppla! Da ist etwas schief gelaufen..\n\n${err.message}', - textAlign: - TextAlign.center, - style: const TextStyle( - color: - Colors.black), - ) - : const Text( - 'Interner Fehler', - textAlign: - TextAlign.center, - style: TextStyle( - color: - Colors.black), - ), - icon: - const Icon( - Icons.error, - color: Colors - .black, - ), - ); - }, - ); - }), - ), - ); - }); - }); - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute( - builder: (builder) => StartPage( - client: widget.client)), - (route) => false); - showDialog( - context: context, - builder: (builder) { - return const AlertDialog( - content: Text( - 'Account wurde angelegt', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.black), - ), - icon: Icon( - Icons.done, - size: 50, - ), - iconColor: Colors.green, - ); - }); - } - }, - ); + // _setLoading(true); } }, child: const Icon(Icons.login)) diff --git a/frontend/app/lib/pages/start_page.dart b/frontend/app/lib/pages/start_page.dart index 751b9dd..39d6ecf 100644 --- a/frontend/app/lib/pages/start_page.dart +++ b/frontend/app/lib/pages/start_page.dart @@ -1,21 +1,26 @@ import 'package:app/gapi/client.dart'; +import 'package:app/model/apis/api_response.dart'; +import 'package:app/model/view_model/account_vm.dart'; import 'package:app/pages/dashboard_page.dart'; import 'package:app/pages/login_page.dart'; import 'package:app/pages/register_page.dart'; +import 'package:app/pb/account.pb.dart'; import 'package:app/widgets/background.dart'; import 'package:app/widgets/bottom_bar.dart'; import 'package:app/widgets/side_drawer.dart'; import 'package:flutter/material.dart'; import 'dart:core'; +import 'package:provider/provider.dart'; + // ignore: must_be_immutable class StartPage extends StatefulWidget { - StartPage({ + const StartPage({ super.key, - required this.client, + // required this.client, }); - GClient client; + // GClient client; @override State createState() => _StartPageState(); @@ -24,320 +29,344 @@ class StartPage extends StatefulWidget { class _StartPageState extends State { final List bottombarButtons = []; - void _updateClient(GClient c) { - setState(() { - widget.client = c; - }); - } + // void _updateClient(GClient c) { + // setState(() { + // widget.client = c; + // }); + // } @override void initState() { super.initState(); } + SnackBar _snackBar(BuildContext context, String message, String label, + void Function() action) { + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + // ScaffoldMessenger.of(context).clearSnackBars(); + return SnackBar( + content: Text( + message, + style: const TextStyle(color: Colors.black), + ), + backgroundColor: Colors.white, + action: SnackBarAction( + label: label, + onPressed: action, + ), + ); + } + @override Widget build(BuildContext context) { return Background( - child: Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - ), - drawer: Builder(builder: (context) { - return SideDrawer(children: [ - const Spacer(), - TextButton( - onPressed: () { - Scaffold.of(context).closeDrawer(); - }, - child: const Row( - children: [ - Text( - 'About', - style: TextStyle(fontSize: 20), - ), - Spacer(), - Icon( - Icons.question_answer, - color: Colors.white, - ), - ], - ), + child: ChangeNotifierProvider( + create: (context) => AccountViewModel(), + child: Consumer(builder: (context, value, child) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, ), - TextButton( - onPressed: () { - Scaffold.of(context).closeDrawer(); - }, - child: const Row( - children: [ - Text( - 'Datenschutz', - style: TextStyle(fontSize: 20), + drawer: Builder(builder: (context) { + return SideDrawer(children: [ + const Spacer(), + TextButton( + onPressed: () { + Scaffold.of(context).closeDrawer(); + }, + child: const Row( + children: [ + Text( + 'About', + style: TextStyle(fontSize: 20), + ), + Spacer(), + Icon( + Icons.question_answer, + color: Colors.white, + ), + ], ), - Spacer(), - Icon( - Icons.privacy_tip, - color: Colors.white, + ), + TextButton( + onPressed: () { + Scaffold.of(context).closeDrawer(); + }, + child: const Row( + children: [ + Text( + 'Datenschutz', + style: TextStyle(fontSize: 20), + ), + Spacer(), + Icon( + Icons.privacy_tip, + color: Colors.white, + ), + ], ), - ], - ), - ), - TextButton( - onPressed: () { - Scaffold.of(context).closeDrawer(); - }, - child: const Row( - children: [ - Text( - 'Impressum', - style: TextStyle(fontSize: 20), + ), + TextButton( + onPressed: () { + Scaffold.of(context).closeDrawer(); + }, + child: const Row( + children: [ + Text( + 'Impressum', + style: TextStyle(fontSize: 20), + ), + Spacer(), + Icon( + Icons.apartment, + color: Colors.white, + ), + ], ), - Spacer(), - Icon( - Icons.apartment, - color: Colors.white, + ), + TextButton( + onPressed: () { + value.logout(); + // ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + _snackBar( + context, + value.response.message != null + ? value.response.message! + : value.response.status.toString(), + value.response.status.toString(), + () { + print('asdf'); + }, + ), + ); + }, + child: const Row( + children: [ + Text( + 'Log out', + style: TextStyle(fontSize: 20), + ), + Spacer(), + Icon( + Icons.logout, + color: Colors.white, + ), + ], ), - ], - ), - ), - TextButton( - onPressed: () { - setState(() { - widget.client.session.accessToken = null; - widget.client.session - .removeSession(widget.client.session.sessionId!); - }); - }, - child: const Row( - children: [ - Text( - 'Log out', - style: TextStyle(fontSize: 20), - ), - Spacer(), - Icon( - Icons.logout, - color: Colors.white, - ), - ], - ), - ), - const SizedBox( - height: 250, - ) - ]); - }), - bottomNavigationBar: Builder(builder: (context) { - return BottomBar( - // onTap: (value) => _bottomBarAction(value), - children: widget.client.session.accessToken != null - ? [ - BottomNavigationBarItem( - backgroundColor: Colors.white, - label: 'Personen', - icon: Column( - children: [ - IconButton( - onPressed: () => Scaffold.of(context).openDrawer(), + ), + const SizedBox( + height: 250, + ) + ]); + }), + bottomNavigationBar: Builder(builder: (context) { + return BottomBar( + // onTap: (value) => _bottomBarAction(value), + children: value.response.data != null + ? [ + BottomNavigationBarItem( + backgroundColor: Colors.white, + label: 'Personen', + icon: Column( + children: [ + IconButton( + onPressed: () => + Scaffold.of(context).openDrawer(), + icon: const Icon( + Icons.group, + color: Colors.white, + ), + ), + const Text( + 'Personen', + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ) + ], + ), + ), + BottomNavigationBarItem( + backgroundColor: Colors.white, + label: 'Dashboard', + icon: Column( + children: [ + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.dashboard, + color: Colors.white, + ), + ), + const Text( + 'Dashboard', + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ) + ], + ), + ), + BottomNavigationBarItem( + backgroundColor: Colors.white, + label: 'Menu', + icon: IconButton( + onPressed: () { + Scaffold.of(context).openDrawer(); + }, icon: const Icon( - Icons.group, + Icons.menu, color: Colors.white, ), ), - const Text( - 'Personen', - style: TextStyle( - color: Colors.white, - fontSize: 16, - ), - ) - ], - ), - ), - BottomNavigationBarItem( - backgroundColor: Colors.white, - label: 'Dashboard', - icon: Column( - children: [ - IconButton( - onPressed: () async { - if (widget.client.session.accessTokenExpiresAt! - .toDateTime() - .isBefore(DateTime.now())) { - await widget.client.refreshToken(); - } - if (!widget.client.isLoggedIn && - context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Sitzung ist abgelaufen.'), - ), - ); - if (context.mounted) { - Navigator.pushAndRemoveUntil( + ) + ] + : [ + BottomNavigationBarItem( + label: 'register', + backgroundColor: Colors.white, + icon: Column( + children: [ + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.login, + color: Colors.white, + ), + ), + const Text( + 'Registrieren', + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ) + ], + ), + ), + BottomNavigationBarItem( + label: 'login', + backgroundColor: Colors.white, + icon: Column( + children: [ + IconButton( + onPressed: () { + Navigator.push( context, MaterialPageRoute( - builder: (context) => - LoginPage(client: widget.client)), - (route) => false); - } - } else { - final ctx = context; - // ignore: use_build_context_synchronously - GClient c = await Navigator.push( - ctx, - MaterialPageRoute( - builder: (context) => DashboardPage( - client: widget.client, - ), - ), - ); - _updateClient(c); - } - }, + builder: (builder) => + const LoginPage())); + }, + icon: const Icon( + Icons.login, + color: Colors.white, + ), + ), + const Text( + 'Login', + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ) + ], + ), + ), + BottomNavigationBarItem( + backgroundColor: Colors.white, + label: 'Menu', + icon: IconButton( + onPressed: () => Scaffold.of(context).openDrawer(), icon: const Icon( - Icons.dashboard, + Icons.menu, color: Colors.white, ), ), + ), + ], + ); + }), + body: Column( + children: [ + if (value.response.status == Status.COMPLETED && + value.response.data != null && + !(value.response.data as Account).emailVerified) + Card( + color: Colors.orange, + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ const Text( - 'Dashboard', + 'E-Mail ist noch nicht verifiziert.', style: TextStyle( + fontWeight: FontWeight.bold, + fontFamily: 'sans-serif', + fontSize: 14), + ), + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.restore, color: Colors.white, - fontSize: 16, ), - ) + ), ], ), ), - BottomNavigationBarItem( - backgroundColor: Colors.white, - label: 'Menu', - icon: IconButton( - onPressed: () { - Scaffold.of(context).openDrawer(); - }, - icon: const Icon( - Icons.menu, - color: Colors.white, + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Image( + image: AssetImage( + 'lib/assets/logo_300x200.png', ), ), - ) - ] - : [ - BottomNavigationBarItem( - label: 'register', - backgroundColor: Colors.white, - icon: Column( - children: [ - IconButton( - onPressed: () async { - widget.client = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => RegisterPage( - client: widget.client, - ))); - }, - icon: const Icon( - Icons.login, - color: Colors.white, - ), - ), - const Text( - 'Registrieren', - style: TextStyle( - color: Colors.white, - fontSize: 16, - ), - ) - ], + const SizedBox( + height: 40, ), - ), - BottomNavigationBarItem( - label: 'login', - backgroundColor: Colors.white, - icon: Column( - children: [ - IconButton( - onPressed: () async { - final c = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => LoginPage( - client: widget.client, - ), - )); - _updateClient(c); - }, - icon: const Icon( - Icons.login, - color: Colors.white, - ), - ), - const Text( - 'Login', - style: TextStyle( - color: Colors.white, - fontSize: 16, - ), - ) - ], - ), - ), - BottomNavigationBarItem( - backgroundColor: Colors.white, - label: 'Menu', - icon: IconButton( - onPressed: () => Scaffold.of(context).openDrawer(), - icon: const Icon( - Icons.menu, - color: Colors.white, + Text( + 'Digitale Spuren auf Knopfdruck entfernen' + .toUpperCase(), + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: 'sans-serif', + fontSize: 24, + height: 1.6, + fontWeight: FontWeight.normal, + letterSpacing: 6, ), ), - ), - ], - ); - }), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Image( - image: AssetImage( - 'lib/assets/logo_300x200.png', - ), - ), - if (widget.client.account != null && - !widget.client.account!.emailVerified) - Container( - height: 120, - width: double.infinity, - padding: - const EdgeInsets.symmetric(vertical: 20, horizontal: 10), - child: Card( - color: Colors.brown.shade300, - child: const Text( - 'Deine E-Mail Adresse ist noch nicht validiert.'), + TextButton( + onPressed: () { + // ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + _snackBar( + context, + value.response.message != null + ? value.response.message! + : value.response.status.toString(), + value.response.status.toString(), + () { + print('asdf'); + }, + ), + ); + }, + child: const Text('asdf')) + ], ), ), - const SizedBox( - height: 40, - ), - Text( - 'Digitale Spuren auf Knopfdruck entfernen'.toUpperCase(), - textAlign: TextAlign.center, - style: const TextStyle( - fontFamily: 'sans-serif', - fontSize: 24, - height: 1.6, - fontWeight: FontWeight.normal, - letterSpacing: 6, - ), - ), - ], - ), - ), + ], + ), + ); + }), ), ); } diff --git a/frontend/app/pubspec.lock b/frontend/app/pubspec.lock index 9323a01..112b12d 100644 --- a/frontend/app/pubspec.lock +++ b/frontend/app/pubspec.lock @@ -195,6 +195,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: "direct main" description: @@ -219,6 +227,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + provider: + dependency: "direct main" + description: + name: provider + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" + source: hosted + version: "6.0.5" sky_engine: dependency: transitive description: flutter diff --git a/frontend/app/pubspec.yaml b/frontend/app/pubspec.yaml index f28d267..2960757 100644 --- a/frontend/app/pubspec.yaml +++ b/frontend/app/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: sqflite: ^2.3.0 path: ^1.8.3 fixnum: ^1.1.0 + provider: ^6.0.5 dev_dependencies: lints: ^2.0.0 From 7750972a421477070f8b92a2caa6a5ed13cc555f Mon Sep 17 00:00:00 2001 From: itsscb Date: Sat, 4 Nov 2023 01:44:49 +0100 Subject: [PATCH 2/5] rf/replaces home_screen and login backs up old screens to pages_old --- frontend/app/lib/data/database.dart | 10 +- frontend/app/lib/main.dart | 10 +- .../lib/model/services/backend_service.dart | 10 +- frontend/app/lib/pages/home_page.dart | 160 ++++++++++++++++++ frontend/app/lib/pages/login_overlay.dart | 134 +++++++++++++++ frontend/app/lib/pages/pages.dart | 1 - .../{pages => pages_old}/dashboard_page.dart | 2 +- .../lib/{pages => pages_old}/login_page.dart | 2 +- .../{pages => pages_old}/register_page.dart | 2 +- .../lib/{pages => pages_old}/start_page.dart | 6 +- .../app/lib/widgets/bottom_navigation.dart | 48 ++++++ .../lib/widgets/bottom_navigation_item.dart | 45 +++++ frontend/app/lib/widgets/drawer.dart | 31 ++++ .../app/lib/widgets/side_drawer_item.dart | 50 ++++++ 14 files changed, 490 insertions(+), 21 deletions(-) create mode 100644 frontend/app/lib/pages/home_page.dart create mode 100644 frontend/app/lib/pages/login_overlay.dart delete mode 100644 frontend/app/lib/pages/pages.dart rename frontend/app/lib/{pages => pages_old}/dashboard_page.dart (99%) rename frontend/app/lib/{pages => pages_old}/login_page.dart (99%) rename frontend/app/lib/{pages => pages_old}/register_page.dart (99%) rename frontend/app/lib/{pages => pages_old}/start_page.dart (98%) create mode 100644 frontend/app/lib/widgets/bottom_navigation.dart create mode 100644 frontend/app/lib/widgets/bottom_navigation_item.dart create mode 100644 frontend/app/lib/widgets/drawer.dart create mode 100644 frontend/app/lib/widgets/side_drawer_item.dart diff --git a/frontend/app/lib/data/database.dart b/frontend/app/lib/data/database.dart index 7d7e411..1ae9a7f 100644 --- a/frontend/app/lib/data/database.dart +++ b/frontend/app/lib/data/database.dart @@ -14,9 +14,7 @@ class Session { this.refreshToken, this.refreshTokenExpiresAt, this.accountId, - }) { - _init(); - } + }); String? sessionId; String? accessToken; @@ -169,12 +167,14 @@ class Session { final db = await database; final List> maps = await db.query('sessions'); - + print(maps); final List sessions = List.generate( maps.length, (i) { // print('GOT MAP: ${maps[i]}'); - + if (maps[i]['sessionId'] == null) { + return Session(); + } return Session( sessionId: maps[i]['sessionId'] as String, accessToken: maps[i]['accessToken'] as String, diff --git a/frontend/app/lib/main.dart b/frontend/app/lib/main.dart index e790121..14f789a 100644 --- a/frontend/app/lib/main.dart +++ b/frontend/app/lib/main.dart @@ -1,6 +1,4 @@ -import 'package:app/gapi/client.dart'; -import 'package:app/pages/start_page.dart'; -import 'package:app/widgets/background.dart'; +import 'package:app/pages/home_page.dart'; import 'package:flutter/material.dart'; void main() async { @@ -43,11 +41,7 @@ void main() async { backgroundColor: Colors.black, foregroundColor: Colors.white, )), - home: Background( - child: StartPage( - // client: await GClient.client, - ), - ), + home: const HomePage(), ), ); } diff --git a/frontend/app/lib/model/services/backend_service.dart b/frontend/app/lib/model/services/backend_service.dart index 6cc42d0..f67b83d 100644 --- a/frontend/app/lib/model/services/backend_service.dart +++ b/frontend/app/lib/model/services/backend_service.dart @@ -202,7 +202,15 @@ class BackendService { password: password, ), ); - await Session.newSession(response as Session); + Session s = Session( + accessToken: response.accessToken, + sessionId: response.sessionId, + accessTokenExpiresAt: response.accessTokenExpiresAt, + refreshToken: response.refreshToken, + refreshTokenExpiresAt: response.refreshTokenExpiresAt, + accountId: response.accountId, + ); + await Session.newSession(s); return response.accessToken != ''; } on SocketException { throw FetchDataException('Keine Internet Verbindung'); diff --git a/frontend/app/lib/pages/home_page.dart b/frontend/app/lib/pages/home_page.dart new file mode 100644 index 0000000..d34c4d6 --- /dev/null +++ b/frontend/app/lib/pages/home_page.dart @@ -0,0 +1,160 @@ +import 'dart:io'; + +import 'package:app/model/services/backend_service.dart'; +import 'package:app/model/view_model/account_vm.dart'; +import 'package:app/pages/login_overlay.dart'; +import 'package:app/widgets/background.dart'; +import 'package:app/widgets/bottom_navigation.dart'; +import 'package:app/widgets/bottom_navigation_item.dart'; +import 'package:app/widgets/drawer.dart'; +import 'package:app/widgets/side_drawer_item.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + @override + void initState() { + super.initState(); + _init(); + } + + void _init() async { + _setLoading(true); + _loggedin = await BackendService.isLoggedIn; + _setLoading(false); + } + + void _setLoading(bool loading) { + setState(() { + _loading = loading; + }); + } + + bool _loading = true; + bool _loggedin = false; + @override + Widget build(BuildContext context) { + return Background( + child: ChangeNotifierProvider( + create: (context) => AccountViewModel(), + child: Consumer( + builder: (context, value, child) => Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + // flexibleSpace: Image.asset( + // 'lib/assets/logo_300x200.png', + // // height: 400, + // ), + ), + drawer: SideDrawer( + children: [ + const Spacer( + flex: 3, + ), + SideDrawerItem( + onPressed: () {}, + icon: Icons.question_answer, + color: Colors.white, + label: 'About', + ), + SideDrawerItem( + onPressed: () {}, + icon: Icons.privacy_tip, + color: Colors.white, + label: 'Datenschutz', + ), + SideDrawerItem( + onPressed: () {}, + icon: Icons.apartment, + color: Colors.white, + label: 'Impressum', + ), + const Spacer( + flex: 1, + ), + if (_loggedin || value.response.data != null) + SideDrawerItem( + onPressed: () {}, + icon: Icons.logout, + color: Colors.white, + label: 'Logout', + ), + ], + ), + bottomNavigationBar: BottomNavigation( + children: [ + if (!_loggedin) ...[ + BottomNavigationItem( + onPressed: () {}, + icon: Icons.person_add_alt, + color: Colors.white, + label: 'Registrieren', + ), + BottomNavigationItem( + onPressed: () async { + _loggedin = await showLogin(context); + }, + icon: Icons.login, + color: Colors.white, + label: 'Login', + ), + ] else + BottomNavigationItem( + onPressed: () {}, + icon: Icons.person_search, + color: Colors.white, + label: 'Personen', + ), + BottomNavigationItem( + onPressed: () {}, + icon: Icons.dashboard, + color: Colors.white, + label: 'Dashboard', + ), + ...[] + ], + ), + body: Padding( + padding: const EdgeInsets.fromLTRB(16, 40, 16, 16), + child: Center( + child: _loading + ? const CircularProgressIndicator( + color: Colors.grey, + ) + : Column( + children: [ + Image.asset( + 'lib/assets/logo_300x200.png', + ), + const SizedBox( + height: 40, + ), + Text( + 'Digitale Spuren auf Knopfdruck entfernen' + .toUpperCase(), + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: 'sans-serif', + fontSize: 24, + height: 1.6, + fontWeight: FontWeight.normal, + letterSpacing: 6, + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/frontend/app/lib/pages/login_overlay.dart b/frontend/app/lib/pages/login_overlay.dart new file mode 100644 index 0000000..21e265f --- /dev/null +++ b/frontend/app/lib/pages/login_overlay.dart @@ -0,0 +1,134 @@ +import 'package:app/model/services/backend_service.dart'; +import 'package:app/widgets/background.dart'; +import 'package:flutter/material.dart'; + +Future showLogin(BuildContext context) async { + final _formKey = GlobalKey(); + final mailController = TextEditingController(); + final passwordController = TextEditingController(); + + bool _loggedin = false; + + await showModalBottomSheet( + useSafeArea: true, + isScrollControlled: true, + backgroundColor: Colors.black, + context: context, + builder: (builder) { + return Background( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + height: 50, + ), + const Image( + width: 180, + image: AssetImage( + 'lib/assets/logo_300x200.png', + ), + ), + const SizedBox( + height: 30, + ), + const Text( + 'Login', + style: TextStyle( + fontFamily: 'sans-serif', + fontSize: 24, + height: 1.6, + fontWeight: FontWeight.normal, + letterSpacing: 6, + ), + ), + Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox( + height: 40, + ), + TextFormField( + controller: mailController, + decoration: const InputDecoration( + fillColor: Color.fromARGB(30, 255, 255, 255), + filled: true, + hintStyle: TextStyle( + color: Colors.white38, + ), + hintText: 'E-Mail Adresse', + ), + keyboardType: TextInputType.emailAddress, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Bitte eine gültige E-Mail Adresse eingeben'; + } + return null; + }, + ), + TextFormField( + style: const TextStyle( + color: Colors.white, + ), + controller: passwordController, + decoration: const InputDecoration( + fillColor: Color.fromARGB(30, 255, 255, 255), + filled: true, + hintStyle: TextStyle( + color: Colors.white38, + ), + hintText: 'Passwort', + ), + keyboardType: TextInputType.visiblePassword, + obscureText: true, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Bitte geben Sie Ihr Passwort ein'; + } + return null; + }, + ), + const SizedBox( + height: 15, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Icon(Icons.arrow_back), + ), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + BackendService.login( + email: mailController.text, + password: passwordController.text, + ).then( + (r) { + if (r) { + _loggedin = r; + Navigator.pop(context, true); + } + }, + ); + } + }, + child: const Icon(Icons.login), + ), + ], + ) + ], + ), + ), + const Spacer(), + ], + ), + ); + }); + return _loggedin; +} diff --git a/frontend/app/lib/pages/pages.dart b/frontend/app/lib/pages/pages.dart deleted file mode 100644 index 6db3efe..0000000 --- a/frontend/app/lib/pages/pages.dart +++ /dev/null @@ -1 +0,0 @@ -enum Pages { start, login, about, persons, dashboard } diff --git a/frontend/app/lib/pages/dashboard_page.dart b/frontend/app/lib/pages_old/dashboard_page.dart similarity index 99% rename from frontend/app/lib/pages/dashboard_page.dart rename to frontend/app/lib/pages_old/dashboard_page.dart index 14d3a25..3c8503b 100644 --- a/frontend/app/lib/pages/dashboard_page.dart +++ b/frontend/app/lib/pages_old/dashboard_page.dart @@ -1,5 +1,5 @@ import 'package:app/gapi/client.dart'; -import 'package:app/pages/start_page.dart'; +import 'package:app/pages_old/start_page.dart'; import 'package:app/pb/account_info.pb.dart'; import 'package:app/pb/rpc_get_account_info.pb.dart'; import 'package:app/widgets/background.dart'; diff --git a/frontend/app/lib/pages/login_page.dart b/frontend/app/lib/pages_old/login_page.dart similarity index 99% rename from frontend/app/lib/pages/login_page.dart rename to frontend/app/lib/pages_old/login_page.dart index b7d49d2..d2207e7 100644 --- a/frontend/app/lib/pages/login_page.dart +++ b/frontend/app/lib/pages_old/login_page.dart @@ -1,6 +1,6 @@ import 'package:app/gapi/client.dart'; import 'package:app/model/services/backend_service.dart'; -import 'package:app/pages/start_page.dart'; +import 'package:app/pages_old/start_page.dart'; import 'package:app/widgets/background.dart'; import 'package:app/widgets/bottom_bar.dart'; import 'package:app/widgets/loading_widget.dart'; diff --git a/frontend/app/lib/pages/register_page.dart b/frontend/app/lib/pages_old/register_page.dart similarity index 99% rename from frontend/app/lib/pages/register_page.dart rename to frontend/app/lib/pages_old/register_page.dart index d5ee106..2a40fd6 100644 --- a/frontend/app/lib/pages/register_page.dart +++ b/frontend/app/lib/pages_old/register_page.dart @@ -1,5 +1,5 @@ import 'package:app/gapi/client.dart'; -import 'package:app/pages/start_page.dart'; +import 'package:app/pages_old/start_page.dart'; import 'package:app/widgets/background.dart'; import 'package:app/widgets/bottom_bar.dart'; import 'package:app/widgets/loading_widget.dart'; diff --git a/frontend/app/lib/pages/start_page.dart b/frontend/app/lib/pages_old/start_page.dart similarity index 98% rename from frontend/app/lib/pages/start_page.dart rename to frontend/app/lib/pages_old/start_page.dart index 39d6ecf..6235c2c 100644 --- a/frontend/app/lib/pages/start_page.dart +++ b/frontend/app/lib/pages_old/start_page.dart @@ -1,9 +1,9 @@ import 'package:app/gapi/client.dart'; import 'package:app/model/apis/api_response.dart'; import 'package:app/model/view_model/account_vm.dart'; -import 'package:app/pages/dashboard_page.dart'; -import 'package:app/pages/login_page.dart'; -import 'package:app/pages/register_page.dart'; +import 'package:app/pages_old/dashboard_page.dart'; +import 'package:app/pages_old/login_page.dart'; +import 'package:app/pages_old/register_page.dart'; import 'package:app/pb/account.pb.dart'; import 'package:app/widgets/background.dart'; import 'package:app/widgets/bottom_bar.dart'; diff --git a/frontend/app/lib/widgets/bottom_navigation.dart b/frontend/app/lib/widgets/bottom_navigation.dart new file mode 100644 index 0000000..25e74b9 --- /dev/null +++ b/frontend/app/lib/widgets/bottom_navigation.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +class BottomNavigation extends StatelessWidget { + BottomNavigation({ + super.key, + required this.children, + this.backgroundColor, + this.iconColor, + }) { + backgroundColor ??= Colors.black; + } + + List children; + Color? backgroundColor; + Color? iconColor; + + @override + Widget build(BuildContext context) { + return Container( + height: 70, + color: backgroundColor, + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric( + // horizontal: 10, + ), + child: Row( + mainAxisAlignment: children.isEmpty + ? MainAxisAlignment.center + : MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ...children, + Builder(builder: (context) { + return IconButton( + onPressed: () => Scaffold.of(context).openDrawer(), + icon: const Icon( + Icons.menu, + color: Colors.white, + )); + }), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/app/lib/widgets/bottom_navigation_item.dart b/frontend/app/lib/widgets/bottom_navigation_item.dart new file mode 100644 index 0000000..72f46c3 --- /dev/null +++ b/frontend/app/lib/widgets/bottom_navigation_item.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +class BottomNavigationItem extends StatelessWidget { + BottomNavigationItem({ + super.key, + required this.onPressed, + required this.icon, + required this.color, + this.textSize, + this.iconSize, + this.label, + }) { + textSize ??= 15; + iconSize ??= 25; + } + + void Function() onPressed; + IconData icon; + Color color; + double? textSize; + double? iconSize; + String? label; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: + label != null ? MainAxisAlignment.center : MainAxisAlignment.center, + children: [ + IconButton( + onPressed: onPressed, + icon: Icon( + icon, + size: iconSize, + color: color, + )), + if (label != null) + Text( + label!, + style: TextStyle(fontSize: textSize, color: color), + ), + ], + ); + } +} diff --git a/frontend/app/lib/widgets/drawer.dart b/frontend/app/lib/widgets/drawer.dart new file mode 100644 index 0000000..5ec955b --- /dev/null +++ b/frontend/app/lib/widgets/drawer.dart @@ -0,0 +1,31 @@ +import 'package:app/pages/home_page.dart'; +import 'package:app/widgets/side_drawer_item.dart'; +import 'package:flutter/material.dart'; + +class SideDrawer extends StatelessWidget { + SideDrawer({super.key, required this.children, this.backgroundColor}) { + backgroundColor ??= Colors.black; + } + + List children; + Color? backgroundColor; + + @override + Widget build(BuildContext context) { + return Drawer( + backgroundColor: backgroundColor, + child: Padding( + padding: const EdgeInsets.all(40.0), + child: Center( + child: Builder(builder: (context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ); + }), + ), + ), + ); + } +} diff --git a/frontend/app/lib/widgets/side_drawer_item.dart b/frontend/app/lib/widgets/side_drawer_item.dart new file mode 100644 index 0000000..35668a4 --- /dev/null +++ b/frontend/app/lib/widgets/side_drawer_item.dart @@ -0,0 +1,50 @@ +import 'package:app/pages/home_page.dart'; +import 'package:flutter/material.dart'; + +class SideDrawerItem extends StatelessWidget { + SideDrawerItem({ + super.key, + required this.onPressed, + required this.icon, + required this.color, + required this.label, + this.textSize, + this.iconSize, + }) { + textSize ??= 15; + iconSize ??= 25; + } + + void Function() onPressed; + IconData icon; + Color color; + String label; + double? textSize; + double? iconSize; + + @override + Widget build(BuildContext context) { + return TextButton( + onPressed: onPressed, + child: Row( + // mainAxisAlignment: MainAxisAlignment.start, + children: [ + Icon( + icon, + color: color, + ), + const SizedBox( + width: 20, + ), + Text( + label, + style: TextStyle( + fontSize: textSize, + color: color, + ), + ) + ], + ), + ); + } +} From ef7f6c093f68ba5feb0c97247212dcf1d6bb00fc Mon Sep 17 00:00:00 2001 From: itsscb Date: Sat, 4 Nov 2023 01:58:15 +0100 Subject: [PATCH 3/5] ft/disables buttons on submit --- frontend/app/lib/pages/login_overlay.dart | 49 +++++++++++++---------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/frontend/app/lib/pages/login_overlay.dart b/frontend/app/lib/pages/login_overlay.dart index 21e265f..6d59622 100644 --- a/frontend/app/lib/pages/login_overlay.dart +++ b/frontend/app/lib/pages/login_overlay.dart @@ -3,11 +3,28 @@ import 'package:app/widgets/background.dart'; import 'package:flutter/material.dart'; Future showLogin(BuildContext context) async { - final _formKey = GlobalKey(); + final formKey = GlobalKey(); final mailController = TextEditingController(); final passwordController = TextEditingController(); - bool _loggedin = false; + bool submitted = false; + bool loggedin = false; + void login() { + if (formKey.currentState!.validate()) { + submitted = true; + BackendService.login( + email: mailController.text, + password: passwordController.text, + ).then( + (r) { + if (r) { + loggedin = r; + Navigator.pop(context, true); + } + }, + ); + } + } await showModalBottomSheet( useSafeArea: true, @@ -42,7 +59,7 @@ Future showLogin(BuildContext context) async { ), ), Form( - key: _formKey, + key: formKey, child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, @@ -97,27 +114,15 @@ Future showLogin(BuildContext context) async { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( - onPressed: () { - Navigator.pop(context); - }, + onPressed: !submitted + ? () { + Navigator.pop(context); + } + : null, child: const Icon(Icons.arrow_back), ), ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - BackendService.login( - email: mailController.text, - password: passwordController.text, - ).then( - (r) { - if (r) { - _loggedin = r; - Navigator.pop(context, true); - } - }, - ); - } - }, + onPressed: !submitted ? login : null, child: const Icon(Icons.login), ), ], @@ -130,5 +135,5 @@ Future showLogin(BuildContext context) async { ), ); }); - return _loggedin; + return loggedin; } From 604c2bdd270b57334114a7c40387c482f57f634c Mon Sep 17 00:00:00 2001 From: itsscb Date: Sun, 5 Nov 2023 22:41:38 +0100 Subject: [PATCH 4/5] ft/adds auto logout and persons_page --- .../lib/model/services/backend_service.dart | 65 ++++- .../app/lib/model/view_model/persons_vm.dart | 29 +++ frontend/app/lib/pages/home_page.dart | 244 +++++++++++------- frontend/app/lib/pages/login_overlay.dart | 48 +++- frontend/app/lib/pages/persons_page.dart | 174 +++++++++++++ frontend/app/lib/util/validation.dart | 27 ++ 6 files changed, 478 insertions(+), 109 deletions(-) create mode 100644 frontend/app/lib/model/view_model/persons_vm.dart create mode 100644 frontend/app/lib/pages/persons_page.dart create mode 100644 frontend/app/lib/util/validation.dart diff --git a/frontend/app/lib/model/services/backend_service.dart b/frontend/app/lib/model/services/backend_service.dart index f67b83d..669bc3e 100644 --- a/frontend/app/lib/model/services/backend_service.dart +++ b/frontend/app/lib/model/services/backend_service.dart @@ -5,9 +5,11 @@ import 'package:app/pb/account.pb.dart'; import 'package:app/pb/account_info.pb.dart'; import 'package:app/pb/person.pb.dart'; import 'package:app/data/database.dart'; +import 'package:app/pb/rpc_create_account.pb.dart'; import 'package:app/pb/rpc_get_account.pb.dart'; import 'package:app/pb/rpc_get_account_info.pb.dart'; import 'package:app/pb/rpc_get_person.pb.dart'; +import 'package:app/pb/rpc_list_persons.pb.dart'; import 'package:app/pb/rpc_login.pb.dart'; import 'package:app/pb/rpc_refresh_token.pb.dart'; import 'package:app/pb/service_df.pbgrpc.dart'; @@ -87,20 +89,48 @@ class BackendService { } if (session.accessTokenExpiresAt == null) { + await logout(); return false; } if (session.refreshTokenExpiresAt == null) { + await logout(); return false; } if (session.refreshTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) { + await logout(); return false; } + if (session.accessTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) { + Session s = await BackendService.refreshToken(session); + if (s == session) { + return false; + } + } + return true; } + static Future createAccount( + {required String email, required String password}) async { + try { + await BackendService.client.createAccount(CreateAccountRequest( + email: email, + password: password, + )); + + return await login(email: email, password: password); + } on SocketException { + throw FetchDataException('Keine Internet Verbindung'); + } on GrpcError catch (err) { + throw FetchDataException(err.message); + } catch (err) { + throw InternalException(err.toString()); + } + } + Future getAccount() async { Session? session = await _isLoggedIn(); if (session == null) { @@ -177,6 +207,34 @@ class BackendService { } } + Future> listPersons() async { + Session session = await Session.session; + if (session.accessTokenExpiresAt == null) { + throw UnauthorizedException('Keine Siztung gefunden'); + } + if (session.accessTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) { + session = await refreshToken(session); + if (session.accessTokenExpiresAt == null) { + throw UnauthorizedException('Sitzung ist abgelaufen'); + } + } + try { + final ListPersonsResponse response = await _client.listPersons( + ListPersonsRequest( + accountId: session.accountId, + ), + options: CallOptions( + metadata: {'Authorization': 'Bearer ${session.accessToken}'})); + return response.persons; + } on SocketException { + throw FetchDataException('Keine Internet Verbindung'); + } on GrpcError catch (err) { + throw FetchDataException(err.message); + } catch (err) { + throw InternalException(err.toString()); + } + } + // Future> listPersons() async { // if (_session.accessToken == null) { // refreshToken(); @@ -221,10 +279,11 @@ class BackendService { } } - Future refreshToken(Session session) async { + static Future refreshToken(Session session) async { try { - final RefreshTokenResponse response = await _client.refreshToken( - RefreshTokenRequest(refreshToken: session.refreshToken)); + final RefreshTokenResponse response = await BackendService.client + .refreshToken( + RefreshTokenRequest(refreshToken: session.refreshToken)); session.accessToken = response.accessToken; session.accessTokenExpiresAt = response.accessTokenExpiresAt; session = await Session.updateToken(session); diff --git a/frontend/app/lib/model/view_model/persons_vm.dart b/frontend/app/lib/model/view_model/persons_vm.dart new file mode 100644 index 0000000..40e4606 --- /dev/null +++ b/frontend/app/lib/model/view_model/persons_vm.dart @@ -0,0 +1,29 @@ +import 'package:app/model/apis/api_response.dart'; +import 'package:app/model/services/backend_service.dart'; +import 'package:app/pb/account.pb.dart'; +import 'package:app/pb/person.pb.dart'; +import 'package:flutter/material.dart'; + +class PersonsViewModel with ChangeNotifier { + PersonsViewModel() { + listPersons(); + } + ApiResponse _apiResponse = ApiResponse.initial('Keine Daten'); + + final BackendService _service = BackendService(); + + ApiResponse get response { + return _apiResponse; + } + + void listPersons() async { + _apiResponse = ApiResponse.loading('Bereite alles vor'); + try { + _apiResponse = + ApiResponse.completed(await _service.listPersons(), 'done'); + } catch (e) { + _apiResponse = ApiResponse.error(e.toString()); + } + notifyListeners(); + } +} diff --git a/frontend/app/lib/pages/home_page.dart b/frontend/app/lib/pages/home_page.dart index d34c4d6..c2eb137 100644 --- a/frontend/app/lib/pages/home_page.dart +++ b/frontend/app/lib/pages/home_page.dart @@ -1,8 +1,8 @@ -import 'dart:io'; - +import 'package:app/model/apis/api_response.dart'; import 'package:app/model/services/backend_service.dart'; import 'package:app/model/view_model/account_vm.dart'; import 'package:app/pages/login_overlay.dart'; +import 'package:app/pages/persons_page.dart'; import 'package:app/widgets/background.dart'; import 'package:app/widgets/bottom_navigation.dart'; import 'package:app/widgets/bottom_navigation_item.dart'; @@ -28,6 +28,12 @@ class _HomePageState extends State { void _init() async { _setLoading(true); _loggedin = await BackendService.isLoggedIn; + // if (!_loggedin) { + // await BackendService.logout(); + // Navigator.of(context).pushAndRemoveUntil( + // MaterialPageRoute(builder: (builder) => HomePage()), + // (route) => false); + // } _setLoading(false); } @@ -37,6 +43,17 @@ class _HomePageState extends State { }); } + void _checkResponse(ApiResponse response) async { + print('${response.message}'); + if (response.status == Status.ERROR && + response.message!.contains('unauthorized')) { + await BackendService.logout(); + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (builder) => const HomePage()), + (route) => false); + } + } + bool _loading = true; bool _loggedin = false; @override @@ -45,114 +62,143 @@ class _HomePageState extends State { child: ChangeNotifierProvider( create: (context) => AccountViewModel(), child: Consumer( - builder: (context, value, child) => Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - // flexibleSpace: Image.asset( - // 'lib/assets/logo_300x200.png', - // // height: 400, - // ), - ), - drawer: SideDrawer( - children: [ - const Spacer( - flex: 3, - ), - SideDrawerItem( - onPressed: () {}, - icon: Icons.question_answer, - color: Colors.white, - label: 'About', - ), - SideDrawerItem( - onPressed: () {}, - icon: Icons.privacy_tip, - color: Colors.white, - label: 'Datenschutz', - ), - SideDrawerItem( - onPressed: () {}, - icon: Icons.apartment, - color: Colors.white, - label: 'Impressum', - ), - const Spacer( - flex: 1, - ), - if (_loggedin || value.response.data != null) + builder: (context, value, child) { + _checkResponse(value.response); + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + // flexibleSpace: Image.asset( + // 'lib/assets/logo_300x200.png', + // // height: 400, + // ), + ), + drawer: SideDrawer( + children: [ + const Spacer( + flex: 3, + ), SideDrawerItem( onPressed: () {}, - icon: Icons.logout, + icon: Icons.question_answer, color: Colors.white, - label: 'Logout', + label: 'About', ), - ], - ), - bottomNavigationBar: BottomNavigation( - children: [ - if (!_loggedin) ...[ - BottomNavigationItem( + SideDrawerItem( onPressed: () {}, - icon: Icons.person_add_alt, + icon: Icons.privacy_tip, color: Colors.white, - label: 'Registrieren', + label: 'Datenschutz', ), - BottomNavigationItem( - onPressed: () async { - _loggedin = await showLogin(context); - }, - icon: Icons.login, - color: Colors.white, - label: 'Login', - ), - ] else - BottomNavigationItem( + SideDrawerItem( onPressed: () {}, - icon: Icons.person_search, + icon: Icons.apartment, color: Colors.white, - label: 'Personen', + label: 'Impressum', ), - BottomNavigationItem( - onPressed: () {}, - icon: Icons.dashboard, - color: Colors.white, - label: 'Dashboard', - ), - ...[] - ], - ), - body: Padding( - padding: const EdgeInsets.fromLTRB(16, 40, 16, 16), - child: Center( - child: _loading - ? const CircularProgressIndicator( - color: Colors.grey, - ) - : Column( - children: [ - Image.asset( - 'lib/assets/logo_300x200.png', - ), - const SizedBox( - height: 40, - ), - Text( - 'Digitale Spuren auf Knopfdruck entfernen' - .toUpperCase(), - textAlign: TextAlign.center, - style: const TextStyle( - fontFamily: 'sans-serif', - fontSize: 24, - height: 1.6, - fontWeight: FontWeight.normal, - letterSpacing: 6, - ), - ), - ], - ), + const Spacer( + flex: 1, + ), + if (_loggedin && value.response.data != null) + SideDrawerItem( + onPressed: () async { + setState(() { + _loading = true; + }); + await BackendService.logout(); + // ignore: use_build_context_synchronously + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (builder) => const HomePage()), + (route) => false); + setState(() { + _loggedin = false; + _loading = false; + }); + }, + icon: Icons.logout, + color: Colors.white, + label: 'Logout', + ), + ], ), - ), - ), + bottomNavigationBar: BottomNavigation( + children: [ + if (!_loggedin) ...[ + BottomNavigationItem( + onPressed: () async { + final bool res = + await showLogin(context, registration: true); + setState(() { + _loggedin = res; + }); + }, + icon: Icons.person_add_alt, + color: Colors.white, + label: 'Registrieren', + ), + BottomNavigationItem( + onPressed: () async { + final bool res = await showLogin(context); + setState(() { + _loggedin = res; + }); + }, + icon: Icons.login, + color: Colors.white, + label: 'Login', + ), + ] else + BottomNavigationItem( + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (builder) => const PersonsPage())); + }, + icon: Icons.person_search, + color: Colors.white, + label: 'Personen', + ), + BottomNavigationItem( + onPressed: () {}, + icon: Icons.dashboard, + color: Colors.white, + label: 'Dashboard', + ), + ...[] + ], + ), + body: Padding( + padding: const EdgeInsets.fromLTRB(16, 40, 16, 16), + child: Center( + child: _loading + ? const CircularProgressIndicator( + color: Colors.grey, + ) + : Column( + children: [ + Image.asset( + 'lib/assets/logo_300x200.png', + ), + const SizedBox( + height: 40, + ), + Text( + 'Digitale Spuren auf Knopfdruck entfernen' + .toUpperCase(), + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: 'sans-serif', + fontSize: 24, + height: 1.6, + fontWeight: FontWeight.normal, + letterSpacing: 6, + ), + ), + ], + ), + ), + ), + ); + }, ), ), ); diff --git a/frontend/app/lib/pages/login_overlay.dart b/frontend/app/lib/pages/login_overlay.dart index 6d59622..927dc46 100644 --- a/frontend/app/lib/pages/login_overlay.dart +++ b/frontend/app/lib/pages/login_overlay.dart @@ -1,8 +1,10 @@ import 'package:app/model/services/backend_service.dart'; import 'package:app/widgets/background.dart'; import 'package:flutter/material.dart'; +import 'package:app/util/validation.dart'; -Future showLogin(BuildContext context) async { +Future showLogin(BuildContext context, + {bool registration = false}) async { final formKey = GlobalKey(); final mailController = TextEditingController(); final passwordController = TextEditingController(); @@ -26,6 +28,23 @@ Future showLogin(BuildContext context) async { } } + void register() { + if (formKey.currentState!.validate()) { + submitted = true; + BackendService.createAccount( + email: mailController.text, + password: passwordController.text, + ).then( + (r) { + if (r) { + loggedin = r; + Navigator.pop(context, true); + } + }, + ); + } + } + await showModalBottomSheet( useSafeArea: true, isScrollControlled: true, @@ -48,9 +67,9 @@ Future showLogin(BuildContext context) async { const SizedBox( height: 30, ), - const Text( - 'Login', - style: TextStyle( + Text( + registration ? 'Registrieren' : 'Login', + style: const TextStyle( fontFamily: 'sans-serif', fontSize: 24, height: 1.6, @@ -68,6 +87,12 @@ Future showLogin(BuildContext context) async { height: 40, ), TextFormField( + autofocus: true, + // inputFormatters: [ + // FilteringTextInputFormatter.allow( + // emailRegExp, + // ), + // ], controller: mailController, decoration: const InputDecoration( fillColor: Color.fromARGB(30, 255, 255, 255), @@ -79,7 +104,7 @@ Future showLogin(BuildContext context) async { ), keyboardType: TextInputType.emailAddress, validator: (value) { - if (value == null || value.isEmpty) { + if (value == null || !value.isValidEmail) { return 'Bitte eine gültige E-Mail Adresse eingeben'; } return null; @@ -89,6 +114,11 @@ Future showLogin(BuildContext context) async { style: const TextStyle( color: Colors.white, ), + // inputFormatters: [ + // FilteringTextInputFormatter.allow( + // passwordRegExp, + // ), + // ], controller: passwordController, decoration: const InputDecoration( fillColor: Color.fromARGB(30, 255, 255, 255), @@ -101,7 +131,7 @@ Future showLogin(BuildContext context) async { keyboardType: TextInputType.visiblePassword, obscureText: true, validator: (value) { - if (value == null || value.isEmpty) { + if (value == null || !value.isValidPassword) { return 'Bitte geben Sie Ihr Passwort ein'; } return null; @@ -122,7 +152,11 @@ Future showLogin(BuildContext context) async { child: const Icon(Icons.arrow_back), ), ElevatedButton( - onPressed: !submitted ? login : null, + onPressed: !submitted + ? !registration + ? login + : register + : null, child: const Icon(Icons.login), ), ], diff --git a/frontend/app/lib/pages/persons_page.dart b/frontend/app/lib/pages/persons_page.dart new file mode 100644 index 0000000..746dfad --- /dev/null +++ b/frontend/app/lib/pages/persons_page.dart @@ -0,0 +1,174 @@ +import 'package:app/model/apis/api_response.dart'; +import 'package:app/model/services/backend_service.dart'; +import 'package:app/model/view_model/persons_vm.dart'; +import 'package:app/pages/home_page.dart'; +import 'package:app/pb/person.pb.dart'; +import 'package:app/widgets/background.dart'; +import 'package:app/widgets/bottom_navigation.dart'; +import 'package:app/widgets/bottom_navigation_item.dart'; +import 'package:app/widgets/side_drawer.dart'; +import 'package:app/widgets/side_drawer_item.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class PersonsPage extends StatefulWidget { + const PersonsPage({super.key}); + + @override + State createState() => _PersonsPageState(); +} + +class _PersonsPageState extends State { + @override + void initState() { + super.initState(); + _init(); + } + + void _init() async { + _setLoading(true); + _loggedin = await BackendService.isLoggedIn; + _setLoading(false); + } + + void _setLoading(bool loading) { + setState(() { + _loading = loading; + }); + } + + void _checkResponse(ApiResponse response) { + if (response.status == Status.ERROR && + response.message!.contains('unauthenticated')) { + BackendService.logout(); + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (builder) => const HomePage()), + (route) => false); + } + } + + bool _loading = true; + bool _loggedin = false; + + List _personsList(List persons) { + final List list = []; + for (var p in persons) { + list.add(Card( + color: Colors.black, + child: Text( + '${p.firstname} ${p.lastname}', + style: const TextStyle(color: Colors.white), + ), + )); + } + return list; + } + + @override + Widget build(BuildContext context) { + return Background( + child: ChangeNotifierProvider( + create: (context) => PersonsViewModel(), + child: Consumer( + builder: (context, value, child) { + _checkResponse(value.response); + return Scaffold( + floatingActionButtonLocation: + FloatingActionButtonLocation.centerFloat, + floatingActionButton: FloatingActionButton( + onPressed: () {}, + child: const Icon(Icons.add), + ), + appBar: AppBar( + automaticallyImplyLeading: false, + ), + drawer: SideDrawer( + children: [ + const Spacer( + flex: 3, + ), + SideDrawerItem( + onPressed: () {}, + icon: Icons.question_answer, + color: Colors.white, + label: 'About', + ), + SideDrawerItem( + onPressed: () {}, + icon: Icons.privacy_tip, + color: Colors.white, + label: 'Datenschutz', + ), + SideDrawerItem( + onPressed: () {}, + icon: Icons.apartment, + color: Colors.white, + label: 'Impressum', + ), + const Spacer( + flex: 1, + ), + if (_loggedin || value.response.data != null) + SideDrawerItem( + onPressed: () async { + setState(() { + _loading = true; + }); + await BackendService.logout(); + // ignore: use_build_context_synchronously + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (builder) => const HomePage()), + (route) => false); + setState(() { + _loggedin = false; + _loading = false; + }); + }, + icon: Icons.logout, + color: Colors.white, + label: 'Logout', + ), + ], + ), + bottomNavigationBar: BottomNavigation( + children: [ + BottomNavigationItem( + onPressed: () {}, + icon: Icons.dashboard, + color: Colors.white, + label: 'Dashboard', + ), + BottomNavigationItem( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: Icons.home, + color: Colors.white, + label: 'Home', + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: _loading + ? const CircularProgressIndicator( + color: Colors.grey, + ) + : value.response.status == Status.COMPLETED + ? value.response.data.length > 0 + ? ListView( + children: _personsList( + (value.response.data as List))) + : const Text('Noch keine Personen angelegt') + : const Text('Lade Daten...'), + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/frontend/app/lib/util/validation.dart b/frontend/app/lib/util/validation.dart new file mode 100644 index 0000000..a7aa9e5 --- /dev/null +++ b/frontend/app/lib/util/validation.dart @@ -0,0 +1,27 @@ +final emailRegExp = RegExp(r"^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+"); +final nameRegExp = + RegExp(r"^\s*([A-Za-z]{1,}([\.,] |[-']| ))+[A-Za-z]+\.?\s*$"); +final phoneRegExp = RegExp(r"^\+?0[0-9]{10}$"); +final passwordRegExp = RegExp(r'^.+$'); + +extension valString on String { + bool get isValidEmail { + return emailRegExp.hasMatch(this); + } + + bool get isValidName { + return nameRegExp.hasMatch(this); + } + + bool get isValidPassword { + return passwordRegExp.hasMatch(this); + } + + bool get isNotEmpty { + return this != trim(); + } + + bool get isValidPhone { + return phoneRegExp.hasMatch(this); + } +} From c4da7bea27db9e0727255a77eb771d225076559f Mon Sep 17 00:00:00 2001 From: itsscb Date: Tue, 7 Nov 2023 00:22:11 +0100 Subject: [PATCH 5/5] ft/adds toasts to api calls --- frontend/app/lib/data/database.dart | 3 +- frontend/app/lib/main.dart | 4 +- frontend/app/lib/model/apis/api_response.dart | 2 +- .../lib/model/services/backend_service.dart | 53 ++- .../app/lib/model/view_model/account_vm.dart | 42 +-- .../app/lib/model/view_model/base_vm.dart | 254 ++++++++++++++ .../app/lib/model/view_model/persons_vm.dart | 38 +- frontend/app/lib/pages/home_page.dart | 326 +++++++++--------- frontend/app/lib/pages/login_overlay.dart | 298 +++++++++------- frontend/app/lib/pages/persons_page.dart | 196 ++++++----- frontend/app/lib/util/colors.dart | 11 + .../app/lib/widgets/bottom_navigation.dart | 1 + .../lib/widgets/bottom_navigation_item.dart | 1 + frontend/app/lib/widgets/drawer.dart | 5 +- .../app/lib/widgets/side_drawer_item.dart | 2 +- 15 files changed, 818 insertions(+), 418 deletions(-) create mode 100644 frontend/app/lib/model/view_model/base_vm.dart create mode 100644 frontend/app/lib/util/colors.dart diff --git a/frontend/app/lib/data/database.dart b/frontend/app/lib/data/database.dart index 1ae9a7f..42b37c8 100644 --- a/frontend/app/lib/data/database.dart +++ b/frontend/app/lib/data/database.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'package:app/pb/session.pbjson.dart'; import 'package:fixnum/fixnum.dart'; import 'package:app/pb/google/protobuf/timestamp.pb.dart'; @@ -167,7 +166,7 @@ class Session { final db = await database; final List> maps = await db.query('sessions'); - print(maps); + // print(maps); final List sessions = List.generate( maps.length, (i) { diff --git a/frontend/app/lib/main.dart b/frontend/app/lib/main.dart index 14f789a..6b36af7 100644 --- a/frontend/app/lib/main.dart +++ b/frontend/app/lib/main.dart @@ -41,7 +41,9 @@ void main() async { backgroundColor: Colors.black, foregroundColor: Colors.white, )), - home: const HomePage(), + home: HomePage( + loggedOut: false, + ), ), ); } diff --git a/frontend/app/lib/model/apis/api_response.dart b/frontend/app/lib/model/apis/api_response.dart index 764c500..6300f64 100644 --- a/frontend/app/lib/model/apis/api_response.dart +++ b/frontend/app/lib/model/apis/api_response.dart @@ -5,7 +5,7 @@ class ApiResponse { ApiResponse.initial(this.message) : status = Status.INITIAL; ApiResponse.loading(this.message) : status = Status.LOADING; - ApiResponse.completed(this.data, this.message) : status = Status.COMPLETED; + ApiResponse.completed(this.data) : status = Status.COMPLETED; ApiResponse.error(this.message) : status = Status.ERROR; @override diff --git a/frontend/app/lib/model/services/backend_service.dart b/frontend/app/lib/model/services/backend_service.dart index 669bc3e..af38df8 100644 --- a/frontend/app/lib/model/services/backend_service.dart +++ b/frontend/app/lib/model/services/backend_service.dart @@ -3,9 +3,11 @@ import 'dart:io'; import 'package:app/model/apis/app_exception.dart'; import 'package:app/pb/account.pb.dart'; import 'package:app/pb/account_info.pb.dart'; +import 'package:app/pb/google/protobuf/timestamp.pb.dart'; import 'package:app/pb/person.pb.dart'; import 'package:app/data/database.dart'; import 'package:app/pb/rpc_create_account.pb.dart'; +import 'package:app/pb/rpc_create_person.pb.dart'; import 'package:app/pb/rpc_get_account.pb.dart'; import 'package:app/pb/rpc_get_account_info.pb.dart'; import 'package:app/pb/rpc_get_person.pb.dart'; @@ -15,7 +17,6 @@ import 'package:app/pb/rpc_refresh_token.pb.dart'; import 'package:app/pb/service_df.pbgrpc.dart'; import 'package:fixnum/fixnum.dart'; import 'package:grpc/grpc.dart'; -import 'package:sqflite/sqflite.dart'; class BackendService { BackendService() { @@ -138,17 +139,16 @@ class BackendService { } try { final GetAccountResponse response = await _client.getAccount( - GetAccountRequest(id: session!.accountId), + GetAccountRequest(id: session.accountId), options: CallOptions( metadata: {'Authorization': 'Bearer ${session.accessToken}'})); return response.account; } on SocketException { throw FetchDataException('Keine Internet Verbindung'); } on GrpcError catch (err) { - // if (err.code == 16) { - // await refreshToken(session); - // return getAccount(); - // } + if (err.code == 16) { + throw UnauthorizedException(err.message); + } throw FetchDataException(err.message); } catch (err) { throw InternalException(err.toString()); @@ -207,6 +207,47 @@ class BackendService { } } + Future createPerson( + {required String firstname, + required String lastname, + required String street, + required String zip, + required String city, + required String country, + required DateTime birthday}) async { + Session session = await Session.session; + if (session.accessTokenExpiresAt == null) { + throw UnauthorizedException('Keine Siztung gefunden'); + } + if (session.accessTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) { + session = await refreshToken(session); + if (session.accessTokenExpiresAt == null) { + throw UnauthorizedException('Sitzung ist abgelaufen'); + } + } + try { + final CreatePersonResponse response = await _client.createPerson( + CreatePersonRequest( + accountId: session.accountId, + lastname: lastname, + firstname: firstname, + street: street, + zip: zip, + country: country, + birthday: Timestamp.fromDateTime(birthday), + ), + options: CallOptions( + metadata: {'Authorization': 'Bearer ${session.accessToken}'})); + return response.person; + } on SocketException { + throw FetchDataException('Keine Internet Verbindung'); + } on GrpcError catch (err) { + throw FetchDataException(err.message); + } catch (err) { + throw InternalException(err.toString()); + } + } + Future> listPersons() async { Session session = await Session.session; if (session.accessTokenExpiresAt == null) { diff --git a/frontend/app/lib/model/view_model/account_vm.dart b/frontend/app/lib/model/view_model/account_vm.dart index 13356fa..fb3a998 100644 --- a/frontend/app/lib/model/view_model/account_vm.dart +++ b/frontend/app/lib/model/view_model/account_vm.dart @@ -1,9 +1,9 @@ import 'package:app/model/apis/api_response.dart'; import 'package:app/model/services/backend_service.dart'; +import 'package:app/model/view_model/base_vm.dart'; import 'package:app/pb/account.pb.dart'; -import 'package:flutter/material.dart'; -class AccountViewModel with ChangeNotifier { +class AccountViewModel extends BaseViewModel { AccountViewModel() { _init(); } @@ -21,36 +21,12 @@ class AccountViewModel with ChangeNotifier { } void _init() async { - _apiResponse = ApiResponse.loading('Bereite alles vor'); - try { - _apiResponse = ApiResponse.completed(await _service.getAccount(), 'done'); - } catch (e) { - _apiResponse = ApiResponse.error(e.toString()); - } - notifyListeners(); - } - - Future fetchAccount() async { - _apiResponse = ApiResponse.loading('Hole Account'); - notifyListeners(); - try { - _apiResponse = ApiResponse.completed(await _service.getAccount(), 'done'); - } catch (e) { - _apiResponse = ApiResponse.error(e.toString()); - } - notifyListeners(); - } - - Future logout() async { - _apiResponse = ApiResponse.loading('Logge aus'); - notifyListeners(); - try { - await BackendService.logout(); - _apiResponse = ApiResponse.completed(null, 'Erfolgreich ausgeloggt'); - } catch (e) { - _apiResponse = ApiResponse.error(e.toString()); - } - print(_apiResponse.message); - notifyListeners(); + super.init(); + // try { + // _apiResponse = ApiResponse.completed(await _service.getAccount()); + // } catch (e) { + // _apiResponse = ApiResponse.error(e.toString()); + // } + // notifyListeners(); } } diff --git a/frontend/app/lib/model/view_model/base_vm.dart b/frontend/app/lib/model/view_model/base_vm.dart new file mode 100644 index 0000000..7745a8e --- /dev/null +++ b/frontend/app/lib/model/view_model/base_vm.dart @@ -0,0 +1,254 @@ +import 'package:app/model/apis/api_response.dart'; +import 'package:app/model/services/backend_service.dart'; +import 'package:app/pages/home_page.dart'; +import 'package:app/util/colors.dart'; +import 'package:flutter/material.dart'; + +class BaseViewModel with ChangeNotifier { + BaseViewModel() { + init(); + } + ApiResponse _apiResponse = ApiResponse.initial('Keine Daten'); + + final BackendService _service = BackendService(); + + ApiResponse get response { + return _apiResponse; + } + + void init() async { + // if (await BackendService.isLoggedIn) { + try { + _apiResponse = ApiResponse.completed(await _service.getAccount()); + } catch (e) { + _apiResponse = ApiResponse.error(e.toString()); + } + notifyListeners(); + // } + } + + Future isLoggedIn(BuildContext context) async { + final messenger = ScaffoldMessenger.of(context); + final navigator = Navigator.of(context); + bool loggedIn = false; + try { + loggedIn = await BackendService.isLoggedIn; + } catch (err) { + if (err.toString().contains('session is blocked')) { + _apiResponse = ApiResponse.error('Sitzung ist abgelaufen'); + navigator.pushAndRemoveUntil( + MaterialPageRoute( + builder: (builder) => HomePage( + loggedOut: true, + )), + (route) => false); + messenger.showSnackBar(SnackBar( + backgroundColor: CustomColors.error, + content: const Text( + 'Sitzung ist abgelaufen', + style: TextStyle(color: Colors.white), + ), + // action: SnackBarAction( + // label: 'Details', + // onPressed: () { + // if (context.mounted) { + // showDialog( + // context: context, + // builder: (context) => AlertDialog( + // backgroundColor: Colors.black, + // icon: Icon( + // Icons.error, + // color: CustomColors.error, + // ), + // content: Text( + // err.toString(), + // textAlign: TextAlign.center, + // ), + // )); + // } + // }, + // ), + )); + } + } + return loggedIn; + } + + Future getAccount(BuildContext context) async { + _apiResponse = ApiResponse.loading('Lade Daten'); + notifyListeners(); + final messenger = ScaffoldMessenger.of(context); + try { + _apiResponse = ApiResponse.completed(await _service.getAccount()); + } catch (e) { + if (e.toString().contains('session is blocked')) { + _apiResponse = ApiResponse.error('Sitzung ist abgelaufen'); + messenger.showSnackBar(SnackBar( + backgroundColor: CustomColors.error, + content: const Text( + 'Sitzung ist abgelaufen', + style: TextStyle(color: Colors.white), + ), + action: SnackBarAction( + label: 'Details', + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: Colors.black, + icon: Icon( + Icons.error, + color: CustomColors.error, + ), + content: Text( + e.toString(), + textAlign: TextAlign.center, + ), + )); + }, + ), + )); + } + messenger.showSnackBar(SnackBar( + backgroundColor: CustomColors.error, + content: const Text( + 'Sitzung ist abgelaufen', + style: TextStyle(color: Colors.white), + ), + action: SnackBarAction( + label: 'Details', + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: Colors.black, + icon: Icon( + Icons.error, + color: CustomColors.error, + ), + content: Text( + e.toString(), + textAlign: TextAlign.center, + ), + )); + }, + ), + )); + _apiResponse = ApiResponse.error(e.toString()); + } + notifyListeners(); + } + + Future logout() async { + _apiResponse = ApiResponse.loading('Logge aus'); + notifyListeners(); + try { + await BackendService.logout(); + _apiResponse = ApiResponse.completed(true); + } catch (e) { + _apiResponse = ApiResponse.error(e.toString()); + } + print(_apiResponse.message); + notifyListeners(); + } + + Future login(BuildContext context, + {required String email, required String password}) async { + bool resp = false; + _apiResponse = ApiResponse.loading('Logge ein'); + notifyListeners(); + final messenger = ScaffoldMessenger.of(context); + try { + resp = await BackendService.login(email: email, password: password); + _apiResponse = ApiResponse.completed(resp); + messenger.showSnackBar(SnackBar( + backgroundColor: CustomColors.success, + content: const Text( + 'Erfolgreich eingeloggt', + style: TextStyle(color: Colors.white), + ), + )); + } catch (e) { + messenger.showSnackBar(SnackBar( + backgroundColor: CustomColors.error, + content: const Text( + 'Login fehlgeschlagen', + style: TextStyle(color: Colors.white), + ), + action: SnackBarAction( + label: 'Details', + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: Colors.black, + icon: Icon( + Icons.error, + color: CustomColors.error, + ), + content: Text( + e.toString(), + textAlign: TextAlign.center, + ), + )); + }, + ), + )); + _apiResponse = ApiResponse.error(e.toString()); + } + print(_apiResponse.message); + notifyListeners(); + return resp; + } + + Future createAccount(BuildContext context, + {required String email, required String password}) async { + bool resp = false; + final messenger = ScaffoldMessenger.of(context); + + _apiResponse = ApiResponse.loading('Logge ein'); + notifyListeners(); + try { + resp = + await BackendService.createAccount(email: email, password: password); + messenger.showSnackBar(SnackBar( + backgroundColor: CustomColors.success, + content: const Text( + 'Account angelegt', + style: TextStyle(color: Colors.white), + ), + )); + _apiResponse = ApiResponse.completed(resp); + } catch (e) { + messenger.showSnackBar(SnackBar( + backgroundColor: CustomColors.error, + content: const Text( + 'Account anlegen fehlgeschlagen', + style: TextStyle(color: Colors.white), + ), + action: SnackBarAction( + label: 'Details', + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: Colors.black, + icon: Icon( + Icons.error, + color: CustomColors.error, + ), + content: Text( + e.toString(), + textAlign: TextAlign.center, + ), + )); + }, + ), + )); + _apiResponse = ApiResponse.error(e.toString()); + } + print(_apiResponse.message); + notifyListeners(); + return resp; + } +} diff --git a/frontend/app/lib/model/view_model/persons_vm.dart b/frontend/app/lib/model/view_model/persons_vm.dart index 40e4606..a8ed578 100644 --- a/frontend/app/lib/model/view_model/persons_vm.dart +++ b/frontend/app/lib/model/view_model/persons_vm.dart @@ -1,6 +1,5 @@ import 'package:app/model/apis/api_response.dart'; import 'package:app/model/services/backend_service.dart'; -import 'package:app/pb/account.pb.dart'; import 'package:app/pb/person.pb.dart'; import 'package:flutter/material.dart'; @@ -16,14 +15,43 @@ class PersonsViewModel with ChangeNotifier { return _apiResponse; } - void listPersons() async { - _apiResponse = ApiResponse.loading('Bereite alles vor'); + Future> listPersons() async { + List persons = []; + _apiResponse = ApiResponse.loading('Lade Daten'); try { - _apiResponse = - ApiResponse.completed(await _service.listPersons(), 'done'); + persons = await _service.listPersons(); + _apiResponse = ApiResponse.completed(persons); } catch (e) { _apiResponse = ApiResponse.error(e.toString()); } notifyListeners(); + return persons; + } + + Future createPerson(BuildContext context, + {required String firstname, + required String lastname, + required String street, + required String zip, + required String city, + required String country, + required DateTime birthday}) async { + Person person = Person(); + _apiResponse = ApiResponse.loading('Erstelle Person'); + try { + person = await _service.createPerson( + firstname: firstname, + lastname: lastname, + street: street, + zip: zip, + city: city, + country: country, + birthday: birthday); + _apiResponse = ApiResponse.completed(person); + } catch (err) { + _apiResponse = ApiResponse.error(err.toString()); + } + notifyListeners(); + return person; } } diff --git a/frontend/app/lib/pages/home_page.dart b/frontend/app/lib/pages/home_page.dart index c2eb137..7b62f12 100644 --- a/frontend/app/lib/pages/home_page.dart +++ b/frontend/app/lib/pages/home_page.dart @@ -1,4 +1,3 @@ -import 'package:app/model/apis/api_response.dart'; import 'package:app/model/services/backend_service.dart'; import 'package:app/model/view_model/account_vm.dart'; import 'package:app/pages/login_overlay.dart'; @@ -11,8 +10,11 @@ import 'package:app/widgets/side_drawer_item.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +// ignore: must_be_immutable class HomePage extends StatefulWidget { - const HomePage({super.key}); + HomePage({super.key, required this.loggedOut}); + + bool loggedOut; @override State createState() => _HomePageState(); @@ -25,182 +27,192 @@ class _HomePageState extends State { _init(); } + AccountViewModel vm = AccountViewModel(); void _init() async { - _setLoading(true); - _loggedin = await BackendService.isLoggedIn; + // _setLoading(true); + // _setLoading(widget.loggedOut); + // _loading = widget.loggedOut; + // _loggedin = await BackendService.isLoggedIn; // if (!_loggedin) { // await BackendService.logout(); - // Navigator.of(context).pushAndRemoveUntil( - // MaterialPageRoute(builder: (builder) => HomePage()), + // final navigator = Navigator.of(context); + // navigator.pushAndRemoveUntil( + // MaterialPageRoute( + // builder: (builder) => HomePage( + // loggedOut: true, + // )), // (route) => false); // } _setLoading(false); } + _isLoggedIn(BuildContext context) async { + bool logged = await vm.isLoggedIn(context); + _loggedin = logged; + } + void _setLoading(bool loading) { setState(() { _loading = loading; }); } - void _checkResponse(ApiResponse response) async { - print('${response.message}'); - if (response.status == Status.ERROR && - response.message!.contains('unauthorized')) { - await BackendService.logout(); - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute(builder: (builder) => const HomePage()), - (route) => false); - } - } - bool _loading = true; bool _loggedin = false; @override Widget build(BuildContext context) { return Background( - child: ChangeNotifierProvider( - create: (context) => AccountViewModel(), - child: Consumer( - builder: (context, value, child) { - _checkResponse(value.response); - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - // flexibleSpace: Image.asset( - // 'lib/assets/logo_300x200.png', - // // height: 400, - // ), - ), - drawer: SideDrawer( - children: [ - const Spacer( - flex: 3, - ), - SideDrawerItem( - onPressed: () {}, - icon: Icons.question_answer, - color: Colors.white, - label: 'About', - ), - SideDrawerItem( - onPressed: () {}, - icon: Icons.privacy_tip, - color: Colors.white, - label: 'Datenschutz', - ), - SideDrawerItem( - onPressed: () {}, - icon: Icons.apartment, - color: Colors.white, - label: 'Impressum', - ), - const Spacer( - flex: 1, - ), - if (_loggedin && value.response.data != null) - SideDrawerItem( - onPressed: () async { - setState(() { - _loading = true; - }); - await BackendService.logout(); - // ignore: use_build_context_synchronously - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (builder) => const HomePage()), - (route) => false); - setState(() { - _loggedin = false; - _loading = false; - }); - }, - icon: Icons.logout, - color: Colors.white, - label: 'Logout', - ), - ], - ), - bottomNavigationBar: BottomNavigation( - children: [ - if (!_loggedin) ...[ - BottomNavigationItem( - onPressed: () async { - final bool res = - await showLogin(context, registration: true); - setState(() { - _loggedin = res; - }); - }, - icon: Icons.person_add_alt, - color: Colors.white, - label: 'Registrieren', - ), - BottomNavigationItem( - onPressed: () async { - final bool res = await showLogin(context); - setState(() { - _loggedin = res; - }); - }, - icon: Icons.login, - color: Colors.white, - label: 'Login', - ), - ] else - BottomNavigationItem( - onPressed: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (builder) => const PersonsPage())); - }, - icon: Icons.person_search, - color: Colors.white, - label: 'Personen', - ), - BottomNavigationItem( - onPressed: () {}, - icon: Icons.dashboard, - color: Colors.white, - label: 'Dashboard', - ), - ...[] - ], - ), - body: Padding( - padding: const EdgeInsets.fromLTRB(16, 40, 16, 16), - child: Center( - child: _loading - ? const CircularProgressIndicator( - color: Colors.grey, - ) - : Column( - children: [ - Image.asset( - 'lib/assets/logo_300x200.png', + child: Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + // flexibleSpace: Image.asset( + // 'lib/assets/logo_300x200.png', + // // height: 400, + // ), + ), + drawer: SideDrawer( + children: [ + const Spacer( + flex: 3, + ), + SideDrawerItem( + onPressed: () {}, + icon: Icons.question_answer, + color: Colors.white, + label: 'About', + ), + SideDrawerItem( + onPressed: () {}, + icon: Icons.privacy_tip, + color: Colors.white, + label: 'Datenschutz', + ), + SideDrawerItem( + onPressed: () {}, + icon: Icons.apartment, + color: Colors.white, + label: 'Impressum', + ), + const Spacer( + flex: 1, + ), + if (_loggedin) + SideDrawerItem( + onPressed: () async { + setState(() { + _loading = true; + }); + await BackendService.logout(); + // ignore: use_build_context_synchronously + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (builder) => HomePage( + loggedOut: true, + )), + (route) => false); + setState(() { + _loggedin = false; + _loading = false; + }); + }, + icon: Icons.logout, + color: Colors.white, + label: 'Logout', + ), + ], + ), + bottomNavigationBar: BottomNavigation( + children: [ + if (!_loggedin) ...[ + BottomNavigationItem( + onPressed: () async { + final bool res = await showLogin(context, registration: true); + setState(() { + _loggedin = res; + }); + }, + icon: Icons.person_add_alt, + color: Colors.white, + label: 'Registrieren', + ), + BottomNavigationItem( + onPressed: () async { + await showLogin(context); + setState(() { + vm.isLoggedIn(context); + }); + }, + icon: Icons.login, + color: Colors.white, + label: 'Login', + ), + ] else + BottomNavigationItem( + onPressed: () async { + final navigator = Navigator.of(context); + if (await vm.isLoggedIn(context)) { + navigator.push(MaterialPageRoute( + builder: (builder) => const PersonsPage())); + } else { + navigator.pushAndRemoveUntil( + MaterialPageRoute( + builder: (builder) => const PersonsPage()), + (route) => false); + } + }, + icon: Icons.person_search, + color: Colors.white, + label: 'Personen', + ), + BottomNavigationItem( + onPressed: () {}, + icon: Icons.dashboard, + color: Colors.white, + label: 'Dashboard', + ), + ...[] + ], + ), + body: Padding( + padding: const EdgeInsets.fromLTRB(16, 45, 16, 16), + child: Center( + child: ChangeNotifierProvider( + create: (context) => AccountViewModel(), + child: + Consumer(builder: (context, value, child) { + // _checkResponse(value.response); + if (!widget.loggedOut) { + _isLoggedIn(context); + } + return _loading + ? const CircularProgressIndicator( + color: Colors.grey, + ) + : Column( + children: [ + Image.asset( + 'lib/assets/logo_300x200.png', + ), + const SizedBox( + height: 40, + ), + Text( + 'Digitale Spuren auf Knopfdruck entfernen' + .toUpperCase(), + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: 'sans-serif', + fontSize: 24, + height: 1.6, + fontWeight: FontWeight.normal, + letterSpacing: 6, ), - const SizedBox( - height: 40, - ), - Text( - 'Digitale Spuren auf Knopfdruck entfernen' - .toUpperCase(), - textAlign: TextAlign.center, - style: const TextStyle( - fontFamily: 'sans-serif', - fontSize: 24, - height: 1.6, - fontWeight: FontWeight.normal, - letterSpacing: 6, - ), - ), - ], - ), - ), - ), - ); - }, + ), + ], + ); + })), ), ), - ); + )); } } diff --git a/frontend/app/lib/pages/login_overlay.dart b/frontend/app/lib/pages/login_overlay.dart index 927dc46..ef927a5 100644 --- a/frontend/app/lib/pages/login_overlay.dart +++ b/frontend/app/lib/pages/login_overlay.dart @@ -1,7 +1,12 @@ -import 'package:app/model/services/backend_service.dart'; +import 'package:app/model/view_model/base_vm.dart'; import 'package:app/widgets/background.dart'; +import 'package:app/widgets/bottom_navigation.dart'; +import 'package:app/widgets/bottom_navigation_item.dart'; +import 'package:app/widgets/side_drawer.dart'; +import 'package:app/widgets/side_drawer_item.dart'; import 'package:flutter/material.dart'; import 'package:app/util/validation.dart'; +import 'package:provider/provider.dart'; Future showLogin(BuildContext context, {bool registration = false}) async { @@ -9,15 +14,17 @@ Future showLogin(BuildContext context, final mailController = TextEditingController(); final passwordController = TextEditingController(); + BaseViewModel vm = BaseViewModel(); bool submitted = false; bool loggedin = false; - void login() { + void login(BuildContext context) { if (formKey.currentState!.validate()) { submitted = true; - BackendService.login( - email: mailController.text, - password: passwordController.text, - ).then( + FocusScope.of(context).unfocus(); + vm + .login(context, + email: mailController.text, password: passwordController.text) + .then( (r) { if (r) { loggedin = r; @@ -25,16 +32,21 @@ Future showLogin(BuildContext context, } }, ); + passwordController.clear(); + submitted = false; } } - void register() { + void register(BuildContext context) { if (formKey.currentState!.validate()) { submitted = true; - BackendService.createAccount( + vm + .createAccount( + context, email: mailController.text, password: passwordController.text, - ).then( + ) + .then( (r) { if (r) { loggedin = r; @@ -52,120 +64,174 @@ Future showLogin(BuildContext context, context: context, builder: (builder) { return Background( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox( - height: 50, - ), - const Image( - width: 180, - image: AssetImage( - 'lib/assets/logo_300x200.png', + child: Scaffold( + drawer: SideDrawer( + children: [ + const Spacer( + flex: 3, ), - ), - const SizedBox( - height: 30, - ), - Text( - registration ? 'Registrieren' : 'Login', - style: const TextStyle( - fontFamily: 'sans-serif', - fontSize: 24, - height: 1.6, - fontWeight: FontWeight.normal, - letterSpacing: 6, + SideDrawerItem( + onPressed: () {}, + icon: Icons.question_answer, + color: Colors.white, + label: 'About', ), - ), - Form( - key: formKey, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox( - height: 40, + SideDrawerItem( + onPressed: () {}, + icon: Icons.privacy_tip, + color: Colors.white, + label: 'Datenschutz', + ), + SideDrawerItem( + onPressed: () {}, + icon: Icons.apartment, + color: Colors.white, + label: 'Impressum', + ), + const Spacer( + flex: 1, + ), + ], + ), + bottomNavigationBar: BottomNavigation( + children: [ + BottomNavigationItem( + onPressed: () { + Navigator.pop(context, false); + }, + icon: Icons.arrow_back, + color: Colors.white, + label: 'Zurück', + ), + BottomNavigationItem( + onPressed: () { + Navigator.pop(context, false); + }, + icon: Icons.home, + color: Colors.white, + label: 'Home', + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + height: 50, + ), + const Image( + width: 180, + image: AssetImage( + 'lib/assets/logo_300x200.png', ), - TextFormField( - autofocus: true, - // inputFormatters: [ - // FilteringTextInputFormatter.allow( - // emailRegExp, - // ), - // ], - controller: mailController, - decoration: const InputDecoration( - fillColor: Color.fromARGB(30, 255, 255, 255), - filled: true, - hintStyle: TextStyle( - color: Colors.white38, - ), - hintText: 'E-Mail Adresse', - ), - keyboardType: TextInputType.emailAddress, - validator: (value) { - if (value == null || !value.isValidEmail) { - return 'Bitte eine gültige E-Mail Adresse eingeben'; - } - return null; - }, + ), + const SizedBox( + height: 30, + ), + Text( + registration ? 'Registrieren' : 'Login', + style: const TextStyle( + fontFamily: 'sans-serif', + fontSize: 24, + height: 1.6, + fontWeight: FontWeight.normal, + letterSpacing: 6, ), - TextFormField( - style: const TextStyle( - color: Colors.white, - ), - // inputFormatters: [ - // FilteringTextInputFormatter.allow( - // passwordRegExp, - // ), - // ], - controller: passwordController, - decoration: const InputDecoration( - fillColor: Color.fromARGB(30, 255, 255, 255), - filled: true, - hintStyle: TextStyle( - color: Colors.white38, - ), - hintText: 'Passwort', - ), - keyboardType: TextInputType.visiblePassword, - obscureText: true, - validator: (value) { - if (value == null || !value.isValidPassword) { - return 'Bitte geben Sie Ihr Passwort ein'; - } - return null; - }, - ), - const SizedBox( - height: 15, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ElevatedButton( - onPressed: !submitted - ? () { - Navigator.pop(context); + ), + ChangeNotifierProvider( + create: (context) => BaseViewModel(), + child: Consumer( + builder: (context, value, child) => Form( + key: formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox( + height: 40, + ), + TextFormField( + // autofocus: true, + // inputFormatters: [ + // FilteringTextInputFormatter.allow( + // emailRegExp, + // ), + // ], + controller: mailController, + decoration: const InputDecoration( + fillColor: Color.fromARGB(30, 255, 255, 255), + filled: true, + hintStyle: TextStyle( + color: Colors.white38, + ), + hintText: 'E-Mail Adresse', + ), + keyboardType: TextInputType.emailAddress, + validator: (value) { + if (value == null || !value.isValidEmail) { + return 'Bitte eine gültige E-Mail Adresse eingeben'; } - : null, - child: const Icon(Icons.arrow_back), + return null; + }, + ), + TextFormField( + style: const TextStyle( + color: Colors.white, + ), + // inputFormatters: [ + // FilteringTextInputFormatter.allow( + // passwordRegExp, + // ), + // ], + controller: passwordController, + decoration: const InputDecoration( + fillColor: Color.fromARGB(30, 255, 255, 255), + filled: true, + hintStyle: TextStyle( + color: Colors.white38, + ), + hintText: 'Passwort', + ), + keyboardType: TextInputType.visiblePassword, + obscureText: true, + validator: (value) { + if (value == null || !value.isValidPassword) { + return 'Bitte geben Sie Ihr Passwort ein'; + } + return null; + }, + ), + const SizedBox( + height: 15, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: !submitted + ? !registration + ? () { + login(context); + } + : () { + register(context); + } + : null, + child: const Icon(Icons.login), + ), + ], + ) + ], ), - ElevatedButton( - onPressed: !submitted - ? !registration - ? login - : register - : null, - child: const Icon(Icons.login), - ), - ], - ) - ], - ), + ), + ), + ), + const Spacer(), + ], ), - const Spacer(), - ], + ), ), ); }); diff --git a/frontend/app/lib/pages/persons_page.dart b/frontend/app/lib/pages/persons_page.dart index 746dfad..d863dec 100644 --- a/frontend/app/lib/pages/persons_page.dart +++ b/frontend/app/lib/pages/persons_page.dart @@ -42,13 +42,23 @@ class _PersonsPageState extends State { response.message!.contains('unauthenticated')) { BackendService.logout(); Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute(builder: (builder) => const HomePage()), + MaterialPageRoute( + builder: (builder) => HomePage( + loggedOut: true, + )), (route) => false); } } bool _loading = true; bool _loggedin = false; + List persons = []; + + PersonsViewModel vm = PersonsViewModel(); + + void listPersons(BuildContext context) async { + persons = await vm.listPersons(); + } List _personsList(List persons) { final List list = []; @@ -67,106 +77,106 @@ class _PersonsPageState extends State { @override Widget build(BuildContext context) { return Background( - child: ChangeNotifierProvider( - create: (context) => PersonsViewModel(), - child: Consumer( - builder: (context, value, child) { - _checkResponse(value.response); - return Scaffold( - floatingActionButtonLocation: - FloatingActionButtonLocation.centerFloat, - floatingActionButton: FloatingActionButton( - onPressed: () {}, - child: const Icon(Icons.add), + child: Scaffold( + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + floatingActionButton: FloatingActionButton( + onPressed: () {}, + child: const Icon(Icons.add), + ), + appBar: AppBar( + automaticallyImplyLeading: false, + ), + drawer: SideDrawer( + children: [ + const Spacer( + flex: 3, + ), + SideDrawerItem( + onPressed: () {}, + icon: Icons.question_answer, + color: Colors.white, + label: 'About', + ), + SideDrawerItem( + onPressed: () {}, + icon: Icons.privacy_tip, + color: Colors.white, + label: 'Datenschutz', + ), + SideDrawerItem( + onPressed: () {}, + icon: Icons.apartment, + color: Colors.white, + label: 'Impressum', + ), + const Spacer( + flex: 1, + ), + if (_loggedin) + SideDrawerItem( + onPressed: () async { + setState(() { + _loading = true; + }); + await BackendService.logout(); + // ignore: use_build_context_synchronously + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (builder) => HomePage( + loggedOut: true, + )), + (route) => false); + setState(() { + _loggedin = false; + _loading = false; + }); + }, + icon: Icons.logout, + color: Colors.white, + label: 'Logout', ), - appBar: AppBar( - automaticallyImplyLeading: false, - ), - drawer: SideDrawer( - children: [ - const Spacer( - flex: 3, - ), - SideDrawerItem( - onPressed: () {}, - icon: Icons.question_answer, - color: Colors.white, - label: 'About', - ), - SideDrawerItem( - onPressed: () {}, - icon: Icons.privacy_tip, - color: Colors.white, - label: 'Datenschutz', - ), - SideDrawerItem( - onPressed: () {}, - icon: Icons.apartment, - color: Colors.white, - label: 'Impressum', - ), - const Spacer( - flex: 1, - ), - if (_loggedin || value.response.data != null) - SideDrawerItem( - onPressed: () async { - setState(() { - _loading = true; - }); - await BackendService.logout(); - // ignore: use_build_context_synchronously - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (builder) => const HomePage()), - (route) => false); - setState(() { - _loggedin = false; - _loading = false; - }); - }, - icon: Icons.logout, - color: Colors.white, - label: 'Logout', - ), - ], - ), - bottomNavigationBar: BottomNavigation( - children: [ - BottomNavigationItem( - onPressed: () {}, - icon: Icons.dashboard, - color: Colors.white, - label: 'Dashboard', - ), - BottomNavigationItem( - onPressed: () { - Navigator.of(context).pop(); - }, - icon: Icons.home, - color: Colors.white, - label: 'Home', - ), - ], - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Center( - child: _loading + ], + ), + bottomNavigationBar: BottomNavigation( + children: [ + BottomNavigationItem( + onPressed: () {}, + icon: Icons.dashboard, + color: Colors.white, + label: 'Dashboard', + ), + BottomNavigationItem( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: Icons.home, + color: Colors.white, + label: 'Home', + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: ChangeNotifierProvider( + create: (context) => PersonsViewModel(), + child: Consumer( + builder: (context, value, child) { + _checkResponse(value.response); + listPersons(context); + return _loading ? const CircularProgressIndicator( color: Colors.grey, ) : value.response.status == Status.COMPLETED ? value.response.data.length > 0 - ? ListView( - children: _personsList( - (value.response.data as List))) + ? ListView(children: _personsList(persons)) : const Text('Noch keine Personen angelegt') - : const Text('Lade Daten...'), - ), + : const Text('Lade Daten...'); + }, ), - ); - }, + ), + ), ), ), ); diff --git a/frontend/app/lib/util/colors.dart b/frontend/app/lib/util/colors.dart new file mode 100644 index 0000000..5c8d297 --- /dev/null +++ b/frontend/app/lib/util/colors.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +class CustomColors { + static Color get error { + return const Color.fromARGB(200, 255, 90, 90); + } + + static Color get success { + return const Color.fromARGB(200, 55, 125, 55); + } +} diff --git a/frontend/app/lib/widgets/bottom_navigation.dart b/frontend/app/lib/widgets/bottom_navigation.dart index 25e74b9..c3f6519 100644 --- a/frontend/app/lib/widgets/bottom_navigation.dart +++ b/frontend/app/lib/widgets/bottom_navigation.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +// ignore: must_be_immutable class BottomNavigation extends StatelessWidget { BottomNavigation({ super.key, diff --git a/frontend/app/lib/widgets/bottom_navigation_item.dart b/frontend/app/lib/widgets/bottom_navigation_item.dart index 72f46c3..c6d6be4 100644 --- a/frontend/app/lib/widgets/bottom_navigation_item.dart +++ b/frontend/app/lib/widgets/bottom_navigation_item.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +// ignore: must_be_immutable class BottomNavigationItem extends StatelessWidget { BottomNavigationItem({ super.key, diff --git a/frontend/app/lib/widgets/drawer.dart b/frontend/app/lib/widgets/drawer.dart index 5ec955b..47343bb 100644 --- a/frontend/app/lib/widgets/drawer.dart +++ b/frontend/app/lib/widgets/drawer.dart @@ -1,13 +1,12 @@ -import 'package:app/pages/home_page.dart'; -import 'package:app/widgets/side_drawer_item.dart'; import 'package:flutter/material.dart'; +// ignore: must_be_immutable class SideDrawer extends StatelessWidget { SideDrawer({super.key, required this.children, this.backgroundColor}) { backgroundColor ??= Colors.black; } - List children; + final List children; Color? backgroundColor; @override diff --git a/frontend/app/lib/widgets/side_drawer_item.dart b/frontend/app/lib/widgets/side_drawer_item.dart index 35668a4..b12e882 100644 --- a/frontend/app/lib/widgets/side_drawer_item.dart +++ b/frontend/app/lib/widgets/side_drawer_item.dart @@ -1,6 +1,6 @@ -import 'package:app/pages/home_page.dart'; import 'package:flutter/material.dart'; +// ignore: must_be_immutable class SideDrawerItem extends StatelessWidget { SideDrawerItem({ super.key,