diff --git a/frontend/app/android/app/build.gradle b/frontend/app/android/app/build.gradle index d6f6037..98ea8be 100644 --- a/frontend/app/android/app/build.gradle +++ b/frontend/app/android/app/build.gradle @@ -45,7 +45,7 @@ android { applicationId "com.example.app" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion + minSdkVersion 18 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/frontend/app/android/app/src/main/AndroidManifest.xml b/frontend/app/android/app/src/main/AndroidManifest.xml index e8847cc..996a45e 100644 --- a/frontend/app/android/app/src/main/AndroidManifest.xml +++ b/frontend/app/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,6 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app/lib/main.dart b/frontend/app/lib/main.dart index 6b36af7..c421f10 100644 --- a/frontend/app/lib/main.dart +++ b/frontend/app/lib/main.dart @@ -1,12 +1,20 @@ -import 'package:app/pages/home_page.dart'; +import 'package:app/model/services/auth_service.dart'; +import 'package:app/model/services/storage_service.dart'; +import 'package:app/pages/start_page.dart'; +import 'package:app/util/colors.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart' + show GlobalMaterialLocalizations; void main() async { WidgetsFlutterBinding.ensureInitialized(); runApp( MaterialApp( + localizationsDelegates: const [GlobalMaterialLocalizations.delegate], + supportedLocales: const [Locale('en'), Locale('de')], theme: ThemeData().copyWith( + canvasColor: Colors.black, colorScheme: const ColorScheme( brightness: Brightness.dark, primary: Colors.white, @@ -41,9 +49,64 @@ void main() async { backgroundColor: Colors.black, foregroundColor: Colors.white, )), - home: HomePage( - loggedOut: false, - ), + home: const DigitalerFrieden(), ), ); } + +class DigitalerFrieden extends StatefulWidget { + const DigitalerFrieden({super.key}); + + @override + State createState() => _DigitalerFriedenState(); +} + +class _DigitalerFriedenState extends State { + final StorageService _storageService = StorageService(); + int? accountLevel; + bool? authenticated; + bool _loading = true; + @override + void initState() { + _init(); + super.initState(); + } + + void _init() async { + accountLevel = await _storageService.accountLevel; + if (accountLevel! > 0) { + authenticated = await AuthService.authenticateWithBiometrics(); + } + setState(() { + _loading = false; + }); + } + + @override + Widget build(BuildContext context) { + if (_loading) { + return SafeArea( + child: Center( + child: Column( + children: [ + const SizedBox( + height: 150, + ), + Hero( + tag: 'logo', + child: Image.asset( + 'assets/JPEG.jpg', + height: 180, + ), + ), + CircularProgressIndicator( + color: CustomColors.primary, + ), + ], + ), + ), + ); + } + return const StartPage(); + } +} diff --git a/frontend/app/lib/model/apis/app_exception.dart b/frontend/app/lib/model/apis/app_exception.dart index 7093126..2fa3844 100644 --- a/frontend/app/lib/model/apis/app_exception.dart +++ b/frontend/app/lib/model/apis/app_exception.dart @@ -4,6 +4,7 @@ class AppException implements Exception { AppException([this._message, this._prefix]); + @override String toString() { return "$_prefix$_message"; } diff --git a/frontend/app/lib/model/services/auth_service.dart b/frontend/app/lib/model/services/auth_service.dart new file mode 100644 index 0000000..1ef3af9 --- /dev/null +++ b/frontend/app/lib/model/services/auth_service.dart @@ -0,0 +1,43 @@ +import 'package:flutter/services.dart'; +import 'package:local_auth/local_auth.dart'; + +class AuthService { + static Future authenticateWithBiometrics() async { + //initialize Local Authentication plugin. + final LocalAuthentication localAuthentication = LocalAuthentication(); + //status of authentication. + bool isAuthenticated = false; + //check if device supports biometrics authentication. + bool isBiometricSupported = await localAuthentication.isDeviceSupported(); + //check if user has enabled biometrics. + //check + bool canCheckBiometrics = await localAuthentication.canCheckBiometrics; + + //if device supports biometrics and user has enabled biometrics, then authenticate. + if (isBiometricSupported && canCheckBiometrics) { + // ignore: use_build_context_synchronously + // final messenger = ScaffoldMessenger.of(context); + try { + isAuthenticated = await localAuthentication.authenticate( + localizedReason: 'Scan your fingerprint to authenticate', + options: const AuthenticationOptions( + biometricOnly: false, + useErrorDialogs: true, + stickyAuth: true, + ), + ); + } on PlatformException catch (err) { + print(err); + // messenger.showSnackBar(SnackBar( + // backgroundColor: CustomColors.error, + // content: Text( + // 'Fehler beim Einrichten der Biometrie: $err', + // style: const TextStyle(color: Colors.white), + // ), + // )); + } + } + print(isAuthenticated); + return isAuthenticated; + } +} diff --git a/frontend/app/lib/model/services/backend_service.dart b/frontend/app/lib/model/services/backend_service.dart index af38df8..8cec418 100644 --- a/frontend/app/lib/model/services/backend_service.dart +++ b/frontend/app/lib/model/services/backend_service.dart @@ -14,6 +14,7 @@ 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/rpc_update_person.pb.dart'; import 'package:app/pb/service_df.pbgrpc.dart'; import 'package:fixnum/fixnum.dart'; import 'package:grpc/grpc.dart'; @@ -114,6 +115,10 @@ class BackendService { return true; } + static Future get accountId async { + return (await Session.session).accountId; + } + static Future createAccount( {required String email, required String password}) async { try { @@ -126,7 +131,7 @@ class BackendService { } on SocketException { throw FetchDataException('Keine Internet Verbindung'); } on GrpcError catch (err) { - throw FetchDataException(err.message); + throw FetchDataException('${err.message}'); } catch (err) { throw InternalException(err.toString()); } @@ -207,14 +212,15 @@ 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 { + Future updatePerson( + {required Int64 id, + String? firstname, + String? lastname, + String? street, + String? zip, + String? city, + String? country, + Timestamp? birthday}) async { Session session = await Session.session; if (session.accessTokenExpiresAt == null) { throw UnauthorizedException('Keine Siztung gefunden'); @@ -226,16 +232,74 @@ class BackendService { } } 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), - ), + final UpdatePersonRequest req = UpdatePersonRequest( + id: id, + ); + + if (lastname != null) { + req.lastname = lastname; + } + if (firstname != null) { + req.firstname = firstname; + } + if (street != null) { + req.street = street; + } + if (city != null) { + req.city = city; + } + if (zip != null) { + req.zip = zip; + } + if (country != null) { + req.country = country; + } + if (birthday != null) { + req.birthday = birthday; + } + final UpdatePersonResponse response = await _client.updatePerson(req, + 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 createPerson( + {required String firstname, + required String lastname, + required String street, + required String zip, + required String city, + required String country, + required Timestamp 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 CreatePersonRequest req = CreatePersonRequest( + accountId: session.accountId, + lastname: lastname, + firstname: firstname, + street: street, + city: city, + zip: zip, + country: country, + birthday: birthday, + ); + final CreatePersonResponse response = await _client.createPerson(req, options: CallOptions( metadata: {'Authorization': 'Bearer ${session.accessToken}'})); return response.person; diff --git a/frontend/app/lib/model/services/storage_service.dart b/frontend/app/lib/model/services/storage_service.dart new file mode 100644 index 0000000..64ee2e2 --- /dev/null +++ b/frontend/app/lib/model/services/storage_service.dart @@ -0,0 +1,84 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class StorageItem { + StorageItem(this.key, this.value); + + final String key; + final String value; +} + +class StorageService { + final _secureStorage = const FlutterSecureStorage( + aOptions: AndroidOptions( + encryptedSharedPreferences: true, + ), + ); + + Future writeData(StorageItem item) async { + await _secureStorage.write( + key: item.key, + value: item.value, + ); + } + + Future readData(String key) async { + return await _secureStorage.read(key: key); + } + + Future> readAllSecureData() async { + var allData = await _secureStorage.readAll(); + List list = + allData.entries.map((e) => StorageItem(e.key, e.value)).toList(); + return list; + } + + Future containsData(String key) async { + return await _secureStorage.containsKey(key: key); + } + + Future deleteSecureData(StorageItem item) async { + await _secureStorage.delete( + key: item.key, + ); + } + + Future deleteAllSecureData() async { + await _secureStorage.deleteAll(); + } + + Future setNotificationSetting(bool enabled) async { + return await writeData(StorageItem('notifications', enabled ? '1' : '0')); + } + + Future get notificationSetting async { + final enabled = await readData('notifications') == '1' ? true : false; + return enabled; + } + + Future get accountLevel async { + int? level; + final lev = await readData('account_level'); + if (lev != null) { + level = int.tryParse(lev); + } + return level ?? 0; + } + + Future setAccountLevel(int level) async { + return await writeData(StorageItem('account_level', '$level')); + } + + Future initAccountLevel() async { + return await writeData(StorageItem('account_level', '0')); + } + + Future addAccountLevel() async { + int? level; + final l = await readData('account_level'); + if (l != null) { + level = int.tryParse(l); + } + return await writeData( + StorageItem('account_level', '${level != null ? level + 1 : 1}')); + } +} diff --git a/frontend/app/lib/model/view_model/account_vm.dart b/frontend/app/lib/model/view_model/account_vm.dart index fb3a998..bbdbbac 100644 --- a/frontend/app/lib/model/view_model/account_vm.dart +++ b/frontend/app/lib/model/view_model/account_vm.dart @@ -7,11 +7,12 @@ class AccountViewModel extends BaseViewModel { AccountViewModel() { _init(); } - ApiResponse _apiResponse = ApiResponse.initial('Keine Daten'); + final ApiResponse _apiResponse = ApiResponse.initial('Keine Daten'); final BackendService _service = BackendService(); Account? _account; + @override ApiResponse get response { return _apiResponse; } @@ -21,7 +22,7 @@ class AccountViewModel extends BaseViewModel { } void _init() async { - super.init(); + // super.init(); // try { // _apiResponse = ApiResponse.completed(await _service.getAccount()); // } catch (e) { diff --git a/frontend/app/lib/model/view_model/base_vm.dart b/frontend/app/lib/model/view_model/base_vm.dart index 7745a8e..370e43d 100644 --- a/frontend/app/lib/model/view_model/base_vm.dart +++ b/frontend/app/lib/model/view_model/base_vm.dart @@ -1,12 +1,12 @@ 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/pages_draft/home_page.dart'; import 'package:app/util/colors.dart'; import 'package:flutter/material.dart'; class BaseViewModel with ChangeNotifier { BaseViewModel() { - init(); + // init(); } ApiResponse _apiResponse = ApiResponse.initial('Keine Daten'); @@ -16,16 +16,16 @@ class BaseViewModel with ChangeNotifier { return _apiResponse; } - void init() async { - // if (await BackendService.isLoggedIn) { - try { - _apiResponse = ApiResponse.completed(await _service.getAccount()); - } catch (e) { - _apiResponse = ApiResponse.error(e.toString()); - } - notifyListeners(); - // } - } + // 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); diff --git a/frontend/app/lib/model/view_model/persons_vm.dart b/frontend/app/lib/model/view_model/persons_vm.dart index a8ed578..f3f5a9e 100644 --- a/frontend/app/lib/model/view_model/persons_vm.dart +++ b/frontend/app/lib/model/view_model/persons_vm.dart @@ -1,6 +1,9 @@ import 'package:app/model/apis/api_response.dart'; import 'package:app/model/services/backend_service.dart'; +import 'package:app/pb/google/protobuf/timestamp.pb.dart'; import 'package:app/pb/person.pb.dart'; +import 'package:app/util/colors.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; class PersonsViewModel with ChangeNotifier { @@ -35,8 +38,9 @@ class PersonsViewModel with ChangeNotifier { required String zip, required String city, required String country, - required DateTime birthday}) async { + required Timestamp birthday}) async { Person person = Person(); + final messenger = ScaffoldMessenger.of(context); _apiResponse = ApiResponse.loading('Erstelle Person'); try { person = await _service.createPerson( @@ -47,8 +51,102 @@ class PersonsViewModel with ChangeNotifier { city: city, country: country, birthday: birthday); + messenger.showSnackBar(SnackBar( + backgroundColor: CustomColors.success, + content: const Text( + 'Gepeichert', + style: TextStyle(color: Colors.white), + ), + )); _apiResponse = ApiResponse.completed(person); } catch (err) { + messenger.showSnackBar(SnackBar( + backgroundColor: CustomColors.error, + content: const Text( + 'Fehler beim Speichern', + 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( + err.toString(), + textAlign: TextAlign.center, + ), + )); + }, + ), + )); + _apiResponse = ApiResponse.error(err.toString()); + } + notifyListeners(); + return person; + } + + Future updatePerson(BuildContext context, + {required Int64 id, + String? firstname, + String? lastname, + String? street, + String? zip, + String? city, + String? country, + Timestamp? birthday}) async { + Person person = Person(); + final messenger = ScaffoldMessenger.of(context); + _apiResponse = ApiResponse.loading('Erstelle Person'); + try { + person = await _service.updatePerson( + id: id, + firstname: firstname, + lastname: lastname, + street: street, + zip: zip, + city: city, + country: country, + birthday: birthday); + messenger.showSnackBar(SnackBar( + backgroundColor: CustomColors.success, + content: const Text( + 'Gepeichert', + style: TextStyle(color: Colors.white), + ), + )); + _apiResponse = ApiResponse.completed(person); + } catch (err) { + messenger.showSnackBar(SnackBar( + backgroundColor: CustomColors.error, + content: const Text( + 'Fehler beim Speichern', + 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( + err.toString(), + textAlign: TextAlign.center, + ), + )); + }, + ), + )); _apiResponse = ApiResponse.error(err.toString()); } notifyListeners(); diff --git a/frontend/app/lib/pages/agb_page.dart b/frontend/app/lib/pages/agb_page.dart new file mode 100644 index 0000000..00378f3 --- /dev/null +++ b/frontend/app/lib/pages/agb_page.dart @@ -0,0 +1,25 @@ +import 'package:app/util/colors.dart'; +import 'package:flutter/material.dart'; + +class AgbPage extends StatelessWidget { + AgbPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + iconTheme: IconThemeData(color: CustomColors.primary), + ), + backgroundColor: Colors.black, + body: const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'TODO: insert AGBs', + textAlign: TextAlign.center, + ) + ], + ), + ); + } +} diff --git a/frontend/app/lib/pages/notifications_page.dart b/frontend/app/lib/pages/notifications_page.dart new file mode 100644 index 0000000..a2f4690 --- /dev/null +++ b/frontend/app/lib/pages/notifications_page.dart @@ -0,0 +1,191 @@ +import 'package:app/model/services/storage_service.dart'; +import 'package:app/pages/registration_page.dart'; +import 'package:app/util/colors.dart'; +import 'package:flutter/material.dart'; + +class NotificationsPage extends StatefulWidget { + const NotificationsPage({super.key}); + + @override + State createState() => _NotificationsPageState(); +} + +class _NotificationsPageState extends State { + final StorageService _storageService = StorageService(); + bool _loading = true; + + Future _setNotificationSetting(bool enabled) async { + await _storageService.setNotificationSetting(enabled); + } + + @override + void initState() { + _init(); + super.initState(); + } + + void _init() async { + final accountLevel = await _storageService.accountLevel; + if (accountLevel > 2 && mounted) { + await Navigator.push(context, + MaterialPageRoute(builder: (builder) => const RegistrationPage())); + setState(() { + _loading = false; + }); + } else { + setState(() { + _loading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: _loading + ? Center( + child: Column( + children: [ + const SizedBox( + height: 150, + ), + Hero( + tag: 'logo', + child: Image.asset( + 'assets/JPEG.jpg', + height: 180, + ), + ), + CircularProgressIndicator( + color: CustomColors.primary, + ), + ], + ), + ) + : Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar( + leading: BackButton( + color: CustomColors.primary, + onPressed: () async { + await _storageService.setAccountLevel(1); + if (mounted) { + Navigator.pop(context); + } + }, + ), + iconTheme: IconThemeData( + color: CustomColors.primary, + ), + ), + body: Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), + child: Column( + // mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + height: 80, + ), + Image.asset('assets/chat_bubbles.png'), + const SizedBox( + height: 60, + ), + const Text( + 'Erhalte Mitteilungen', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'sans-serif', + fontWeight: FontWeight.bold, + letterSpacing: 2.0, + fontSize: 25, + ), + ), + const SizedBox( + height: 20, + ), + const Text( + 'Du erhältst z. B. eine Mitteilung sobald wir eine Digitale Spur gefunden haben.', + textAlign: TextAlign.center, + ), + const SizedBox( + height: 20, + ), + const Text( + 'Du kannst die Mitteilungen jederzeit wieder deaktivieren.', + textAlign: TextAlign.center, + ), + const Spacer( + flex: 2, + ), + Hero( + tag: 'flow-button', + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: CustomColors.primary, + ), + onPressed: () async { + await _setNotificationSetting(true); + await _storageService.setAccountLevel(3); + if (mounted) { + Navigator.push( + context, + MaterialPageRoute( + builder: (builder) => const RegistrationPage(), + // builder: (builder) => SecurityPage(), + ), + ); + } + }, + child: const SizedBox( + height: 60, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + flex: 1, + child: Text( + 'Mitteilungen erhalten', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + ), + ), + ), + ], + ), + ), + ), + ), + const SizedBox( + height: 10, + ), + TextButton( + onPressed: () async { + await _setNotificationSetting(false); + await _storageService.setAccountLevel(3); + if (mounted) { + Navigator.push( + context, + MaterialPageRoute( + builder: (builder) => const RegistrationPage(), + // builder: (builder) => SecurityPage(), + ), + ); + } + }, + child: Text( + 'Später', + style: TextStyle(color: CustomColors.primary), + ), + ), + const Spacer( + flex: 1, + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/app/lib/pages/password_page.dart b/frontend/app/lib/pages/password_page.dart new file mode 100644 index 0000000..9767dc4 --- /dev/null +++ b/frontend/app/lib/pages/password_page.dart @@ -0,0 +1,244 @@ +import 'package:app/model/services/storage_service.dart'; +import 'package:app/model/view_model/base_vm.dart'; +import 'package:app/pages/verify_email_page.dart'; +import 'package:app/util/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:app/util/validation.dart'; + +class PasswordPage extends StatefulWidget { + const PasswordPage({super.key, required this.email, required this.register}); + + final String email; + final bool register; + + @override + State createState() => _PasswordPageState(); +} + +class _PasswordPageState extends State { + final BaseViewModel _vm = BaseViewModel(); + final StorageService _storageService = StorageService(); + final _formKey = GlobalKey(); + final _passwordController1 = TextEditingController(); + + final _passwordController2 = TextEditingController(); + + bool _validPassword = false; + + bool _passwordsFilled = false; + + bool _showPassword1 = false; + bool _showPassword2 = false; + + @override + void dispose() { + _passwordController1.dispose(); + _passwordController2.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + iconTheme: IconThemeData( + color: CustomColors.primary, + ), + ), + body: Padding( + padding: const EdgeInsets.all(20.0), + child: SingleChildScrollView( + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.register + ? 'Sichere dein Konto mit einem Passwort' + : 'Login', + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: 'sans-serif', + fontWeight: FontWeight.bold, + letterSpacing: 2.0, + fontSize: 25, + ), + ), + const SizedBox( + height: 60, + ), + TextFormField( + controller: _passwordController1, + autocorrect: false, + autofocus: true, + keyboardType: TextInputType.visiblePassword, + obscureText: !_showPassword1, + autovalidateMode: AutovalidateMode.always, + decoration: InputDecoration( + suffixIcon: IconButton( + onPressed: () { + setState(() { + _showPassword1 = !_showPassword1; + }); + }, + icon: Icon(_showPassword1 + ? Icons.remove_red_eye + : Icons.remove_red_eye_outlined)), + label: const Text('Passwort'), + filled: true, + ), + validator: (value) { + if (value == null || !value.isValidPassword) { + _validPassword = false; + return 'Mindestens 12 Zeichen, Zahlen, Sonderzeichen (-_?!=.,*+), Groß- & Kleinbuchstaben'; + } else { + if (!widget.register) { + _passwordsFilled = true; + } + _validPassword = true; + return null; + } + }, + onChanged: (value) { + _formKey.currentState?.validate(); + if (!value.isValidPassword) { + setState(() { + _validPassword = false; + }); + } else { + setState(() { + if (!widget.register) { + _passwordsFilled = true; + } + _validPassword = true; + }); + } + }, + ), + const SizedBox( + height: 20, + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: !widget.register || !_validPassword + ? null + : TextFormField( + controller: _passwordController2, + keyboardType: TextInputType.visiblePassword, + obscureText: !_showPassword2, + decoration: InputDecoration( + suffixIcon: IconButton( + onPressed: () { + setState(() { + _showPassword2 = !_showPassword2; + }); + }, + icon: Icon(_showPassword2 + ? Icons.remove_red_eye + : Icons.remove_red_eye_outlined)), + label: const Text('Passwort bestätigen'), + filled: true, + ), + validator: (value) { + if (_passwordController1.text != + _passwordController2.text) { + setState(() { + _passwordsFilled = false; + }); + return 'Passwörter stimmen nicht überein'; + } else { + setState(() { + _passwordsFilled = true; + }); + return null; + } + }, + onTap: () => _formKey.currentState?.validate(), + onChanged: (value) { + _formKey.currentState?.validate(); + if (_passwordController1.text != + _passwordController2.text) { + setState(() { + _passwordsFilled = false; + }); + } else { + setState(() { + _passwordsFilled = true; + }); + } + }, + ), + ), + const SizedBox( + height: 20, + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: !_passwordsFilled || + (widget.register && + _passwordController1.text != + _passwordController2.text) + ? null + : ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: CustomColors.primary, + ), + onPressed: _validPassword && _passwordsFilled + ? () async { + if (_formKey.currentState!.validate()) { + FocusScope.of(context).unfocus(); + final navigator = Navigator.of(context); + bool loggedin = false; + if (widget.register) { + loggedin = await _vm.createAccount( + context, + email: widget.email, + password: _passwordController1.text, + ); + } else { + loggedin = await _vm.login( + context, + email: widget.email, + password: _passwordController1.text, + ); + } + if (loggedin && mounted) { + await _storageService.setAccountLevel(4); + navigator.push( + MaterialPageRoute( + builder: (builder) => + const VerifyEmailPage(), + ), + ); + } + } + } + : null, + child: SizedBox( + height: 50, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.register + ? 'Registrierung abschließen' + : 'Einloggen', + style: const TextStyle( + fontSize: 20, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/frontend/app/lib/pages/registration_page.dart b/frontend/app/lib/pages/registration_page.dart new file mode 100644 index 0000000..dd66e8c --- /dev/null +++ b/frontend/app/lib/pages/registration_page.dart @@ -0,0 +1,215 @@ +import 'package:app/model/services/storage_service.dart'; +import 'package:app/pages/password_page.dart'; +import 'package:app/pages/verify_email_page.dart'; +import 'package:app/util/colors.dart'; +import 'package:app/util/validation.dart'; +import 'package:flutter/material.dart'; + +class RegistrationPage extends StatefulWidget { + const RegistrationPage({super.key}); + + @override + State createState() => _RegistrationPageState(); +} + +class _RegistrationPageState extends State { + final formKey = GlobalKey(); + final mailController = TextEditingController(); + bool _loading = true; + + final StorageService _storageService = StorageService(); + + @override + void initState() { + _init(); + super.initState(); + } + + void _init() async { + final accountLevel = await _storageService.accountLevel; + if (accountLevel > 3 && mounted) { + await Navigator.push(context, + MaterialPageRoute(builder: (builder) => const VerifyEmailPage())); + setState(() { + _loading = false; + }); + } else { + setState(() { + _loading = false; + }); + } + } + + @override + void dispose() { + mailController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: _loading + ? Center( + child: Column( + children: [ + const SizedBox( + height: 150, + ), + Hero( + tag: 'logo', + child: Image.asset( + 'assets/JPEG.jpg', + height: 180, + ), + ), + CircularProgressIndicator( + color: CustomColors.primary, + ), + ], + ), + ) + : Scaffold( + appBar: AppBar( + leading: BackButton( + color: CustomColors.primary, + onPressed: () async { + await _storageService.setAccountLevel(2); + if (mounted) { + Navigator.pop(context); + } + }, + ), + iconTheme: IconThemeData( + color: CustomColors.primary, + ), + ), + body: Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), + child: Column( + // mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + height: 20, + ), + const Text( + 'Jetzt Registrieren', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'sans-serif', + fontWeight: FontWeight.bold, + letterSpacing: 2.0, + fontSize: 25, + ), + ), + const SizedBox( + height: 20, + ), + const Text( + 'Gib deine E-Mail Adresse ein.', + // textAlign: TextAlign.center, + ), + const SizedBox( + height: 20, + ), + Form( + key: formKey, + child: Column( + children: [ + TextFormField( + autocorrect: false, + autofocus: true, + controller: mailController, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration( + helperText: 'test', + label: Text('E-Mail Adresse'), + filled: true, + ), + validator: (value) { + if (value == null || !value.isValidEmail) { + return 'Bitte eine valide E-Mail Adresse angeben'; + } else { + return null; + } + }, + ), + ], + ), + ), + const SizedBox( + height: 20, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () { + if (formKey.currentState!.validate()) { + Navigator.push( + context, + MaterialPageRoute( + builder: (builder) => PasswordPage( + email: mailController.text, + register: false, + ), + ), + ); + } + }, + child: Text( + 'Stattdessen anmelden', + // textAlign: TextAlign.center, + style: TextStyle( + color: CustomColors.primary, + ), + ), + ), + Hero( + tag: 'flow-button', + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: CustomColors.primary, + ), + onPressed: () { + if (formKey.currentState!.validate()) { + Navigator.push( + context, + MaterialPageRoute( + builder: (builder) => PasswordPage( + email: mailController.text, + register: true, + ), + ), + ); + } + }, + child: const SizedBox( + height: 50, + width: 100, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Weiter', + style: TextStyle( + fontSize: 20, + ), + ), + ], + ), + ), + ), + ), + ], + ), + const Spacer( + flex: 2, + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/app/lib/pages/security_page.dart b/frontend/app/lib/pages/security_page.dart new file mode 100644 index 0000000..fd10999 --- /dev/null +++ b/frontend/app/lib/pages/security_page.dart @@ -0,0 +1,187 @@ +import 'package:app/model/services/auth_service.dart'; +import 'package:app/model/services/storage_service.dart'; +import 'package:app/pages/notifications_page.dart'; +import 'package:app/util/colors.dart'; +import 'package:flutter/material.dart'; + +class SecurityPage extends StatefulWidget { + const SecurityPage({super.key}); + + @override + State createState() => _SecurityPageState(); +} + +class _SecurityPageState extends State { + final StorageService _storageService = StorageService(); + bool _loading = true; + + @override + void initState() { + _init(); + super.initState(); + } + + void _init() async { + final accountLevel = await _storageService.accountLevel; + if (accountLevel > 1 && mounted) { + await Navigator.push(context, + MaterialPageRoute(builder: (builder) => const NotificationsPage())); + setState(() { + _loading = false; + }); + } else { + setState(() { + _loading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: _loading + ? Center( + child: Column( + children: [ + const SizedBox( + height: 150, + ), + Hero( + tag: 'logo', + child: Image.asset( + 'assets/JPEG.jpg', + height: 180, + ), + ), + CircularProgressIndicator( + color: CustomColors.primary, + ), + ], + ), + ) + : Scaffold( + appBar: AppBar( + leading: BackButton( + color: CustomColors.primary, + onPressed: () async { + await _storageService.setAccountLevel(0); + if (mounted) { + Navigator.pop(context); + } + }, + ), + iconTheme: IconThemeData(color: CustomColors.primary), + ), + body: Padding( + padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), + child: Center( + child: Column( + children: [ + const Spacer(), + const Hero( + tag: 'flow-icon', + child: Icon( + Icons.fingerprint, + color: Colors.white, + size: 200, + ), + ), + const Spacer(), + const Text( + 'Deine Sicherheit kommt an erster Stelle', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'sans-serif', + fontSize: 25, + fontWeight: FontWeight.bold), + ), + const SizedBox( + height: 30, + ), + const Text( + 'Schütze dein Konto mit der biometrischen Erkennung deines Geräts oder lege einen Code fest.', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'sans-serif', + fontSize: 18, + fontWeight: FontWeight.bold), + ), + const Spacer(), + Hero( + tag: 'flow-button', + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: CustomColors.primary, + ), + onPressed: () async { + bool isAuthenticated = + await AuthService.authenticateWithBiometrics(); + if (isAuthenticated) { + await _storageService.setAccountLevel(2); + // ignore: use_build_context_synchronously + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const NotificationsPage()), + ); + } + }, + child: const SizedBox( + height: 60, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'App absichern', + style: TextStyle( + fontSize: 20, + ), + ), + ], + ), + ), + ), + ), + // const SizedBox( + // height: 10, + // ), + // ElevatedButton( + // style: ElevatedButton.styleFrom( + // backgroundColor: CustomColors.secondary, + // ), + // onPressed: () { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (builder) => SecurityPage(), + // ), + // ); + // }, + // child: const SizedBox( + // height: 60, + // child: Row( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // Text( + // 'Eigenen Code festlegen', + // style: TextStyle( + // color: Colors.white, + // fontSize: 22, + // ), + // ), + // ], + // ), + // ), + // ), + const Spacer( + flex: 2, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/frontend/app/lib/pages/start_page.dart b/frontend/app/lib/pages/start_page.dart new file mode 100644 index 0000000..65fbf89 --- /dev/null +++ b/frontend/app/lib/pages/start_page.dart @@ -0,0 +1,166 @@ +import 'package:app/model/services/storage_service.dart'; +import 'package:app/pages/agb_page.dart'; +import 'package:app/pages/security_page.dart'; +import 'package:app/util/colors.dart'; +import 'package:flutter/material.dart'; + +class StartPage extends StatefulWidget { + const StartPage({super.key}); + + @override + State createState() => _StartPageState(); +} + +class _StartPageState extends State { + final StorageService _storageService = StorageService(); + bool _loading = true; + + @override + void initState() { + _init(); + super.initState(); + } + + void _init() async { + int accountLevel = await _storageService.accountLevel; + if (accountLevel > 0 && mounted) { + await Navigator.push(context, + MaterialPageRoute(builder: (builder) => const SecurityPage())); + setState(() { + _loading = false; + }); + } else { + setState(() { + _loading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: _loading + ? Center( + child: Column( + children: [ + const SizedBox( + height: 150, + ), + Hero( + tag: 'logo', + child: Image.asset( + 'assets/JPEG.jpg', + height: 180, + ), + ), + CircularProgressIndicator( + color: CustomColors.primary, + ), + ], + ), + ) + : Scaffold( + appBar: AppBar( + iconTheme: IconThemeData(color: CustomColors.primary), + ), + body: Padding( + padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), + child: Column( + children: [ + Hero( + tag: 'flow-icon', + child: Image.asset( + 'assets/JPEG.jpg', + height: 180, + ), + ), + const SizedBox( + height: 30, + ), + const Text( + 'Hallo. Digitale Spuren\nentfernen\nper Knopfdruck.', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'sans-serif', + fontWeight: FontWeight.bold, + letterSpacing: 2.0, + fontSize: 25, + ), + ), + const SizedBox( + height: 20, + ), + const Text( + 'Mit uns finden Sie Ihre Digitalen Spuren und können diese entfernen.', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'sans-serif', + fontSize: 18, + ), + ), + const Spacer( + flex: 1, + ), + Hero( + tag: 'flow-button', + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: CustomColors.primary, + ), + onPressed: () async { + await _storageService.setAccountLevel(1); + if (mounted) { + Navigator.push( + context, + MaterialPageRoute( + builder: (builder) => const SecurityPage(), + // builder: (builder) => SecurityPage(), + ), + ); + } + }, + child: const SizedBox( + height: 60, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Weiter', + style: TextStyle( + fontSize: 20, + ), + ), + ], + ), + ), + ), + ), + const Spacer( + flex: 1, + ), + const Text( + 'Mit der weiteren Nutzung stimmst du den folgenden Bedingungen zu:', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'sans-serif', + fontSize: 16, + ), + ), + TextButton( + onPressed: () { + showDialog( + context: context, + builder: (builder) => AgbPage()); + }, + child: Text( + 'AGB - Datenschutzerklärung', + textAlign: TextAlign.center, + style: TextStyle(color: CustomColors.primary), + )) + ], + ), + ), + ), + ); + } +} diff --git a/frontend/app/lib/pages/verify_email_page.dart b/frontend/app/lib/pages/verify_email_page.dart new file mode 100644 index 0000000..ddc26c4 --- /dev/null +++ b/frontend/app/lib/pages/verify_email_page.dart @@ -0,0 +1,156 @@ +import 'package:app/model/services/storage_service.dart'; +import 'package:app/util/colors.dart'; +import 'package:flutter/material.dart'; + +class VerifyEmailPage extends StatefulWidget { + const VerifyEmailPage({super.key}); + + @override + State createState() => _VerifyEmailPageState(); +} + +class _VerifyEmailPageState extends State { + final StorageService _storageService = StorageService(); + bool _loading = true; + + @override + void initState() { + super.initState(); + setState(() { + _loading = false; + }); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: _loading + ? Center( + child: Column( + children: [ + const SizedBox( + height: 150, + ), + Hero( + tag: 'logo', + child: Image.asset( + 'assets/JPEG.jpg', + height: 180, + ), + ), + CircularProgressIndicator( + color: CustomColors.primary, + ), + ], + ), + ) + : Scaffold( + appBar: AppBar( + leading: BackButton( + color: CustomColors.primary, + onPressed: () async { + await _storageService.setAccountLevel(3); + if (mounted) { + Navigator.pop(context); + } + }, + ), + iconTheme: IconThemeData( + color: CustomColors.primary, + ), + ), + body: Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), + child: Column( + // mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + height: 80, + ), + const Text( + 'Verifizieren', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'sans-serif', + fontWeight: FontWeight.bold, + letterSpacing: 2.0, + fontSize: 25, + ), + ), + const SizedBox( + height: 50, + ), + const Text( + 'Wir haben dir eine E-Mail geschickt.', + textAlign: TextAlign.center, + // textAlign: TextAlign.center, + ), + const SizedBox( + height: 20, + ), + const Text( + 'Bitte verifiziere deine E-Mail Adresse, dann geht es weiter.', + textAlign: TextAlign.center, + // textAlign: TextAlign.center, + ), + const SizedBox( + height: 80, + ), + Hero( + tag: 'flow-button', + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: CustomColors.primary, + ), + onPressed: () {}, + child: const SizedBox( + height: 50, + width: 100, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Weiter', + style: TextStyle( + fontSize: 20, + ), + ), + ], + ), + ), + ), + ), + const SizedBox( + height: 60, + ), + const Text( + 'Noch keine E-Mail erhalten?', + // textAlign: TextAlign.center, + ), + const Text( + 'Schon im Spam-Ordner nachgeschaut?', + // textAlign: TextAlign.center, + ), + const SizedBox( + height: 20, + ), + TextButton( + onPressed: () {}, + child: Text( + 'Erneut senden', + // textAlign: TextAlign.center, + style: TextStyle( + color: CustomColors.primary, + ), + ), + ), + const Spacer( + flex: 2, + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/app/lib/pages/home_page.dart b/frontend/app/lib/pages_draft/home_page.dart similarity index 92% rename from frontend/app/lib/pages/home_page.dart rename to frontend/app/lib/pages_draft/home_page.dart index 7b62f12..0774069 100644 --- a/frontend/app/lib/pages/home_page.dart +++ b/frontend/app/lib/pages_draft/home_page.dart @@ -1,7 +1,8 @@ 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/model/view_model/base_vm.dart'; +import 'package:app/pages_draft/login_overlay.dart'; +import 'package:app/pages_draft/persons_page.dart'; import 'package:app/widgets/background.dart'; import 'package:app/widgets/bottom_navigation.dart'; import 'package:app/widgets/bottom_navigation_item.dart'; @@ -48,7 +49,9 @@ class _HomePageState extends State { _isLoggedIn(BuildContext context) async { bool logged = await vm.isLoggedIn(context); - _loggedin = logged; + setState(() { + _loggedin = logged; + }); } void _setLoading(bool loading) { @@ -137,9 +140,10 @@ class _HomePageState extends State { ), BottomNavigationItem( onPressed: () async { - await showLogin(context); + bool res = await showLogin(context); setState(() { - vm.isLoggedIn(context); + _loggedin = res; + // vm.isLoggedIn(context); }); }, icon: Icons.login, @@ -150,7 +154,7 @@ class _HomePageState extends State { BottomNavigationItem( onPressed: () async { final navigator = Navigator.of(context); - if (await vm.isLoggedIn(context)) { + if (_loggedin) { navigator.push(MaterialPageRoute( builder: (builder) => const PersonsPage())); } else { @@ -176,10 +180,9 @@ class _HomePageState extends State { body: Padding( padding: const EdgeInsets.fromLTRB(16, 45, 16, 16), child: Center( - child: ChangeNotifierProvider( - create: (context) => AccountViewModel(), - child: - Consumer(builder: (context, value, child) { + child: ChangeNotifierProvider( + create: (context) => BaseViewModel(), + child: Consumer(builder: (context, value, child) { // _checkResponse(value.response); if (!widget.loggedOut) { _isLoggedIn(context); diff --git a/frontend/app/lib/pages/login_overlay.dart b/frontend/app/lib/pages_draft/login_overlay.dart similarity index 100% rename from frontend/app/lib/pages/login_overlay.dart rename to frontend/app/lib/pages_draft/login_overlay.dart diff --git a/frontend/app/lib/pages_draft/person_details_page.dart b/frontend/app/lib/pages_draft/person_details_page.dart new file mode 100644 index 0000000..86f38b6 --- /dev/null +++ b/frontend/app/lib/pages_draft/person_details_page.dart @@ -0,0 +1,339 @@ +import 'package:app/model/view_model/persons_vm.dart'; +import 'package:app/pb/google/protobuf/timestamp.pb.dart'; +import 'package:app/pb/person.pb.dart'; +import 'package:app/util/validation.dart'; +import 'package:app/widgets/background.dart'; +import 'package:app/widgets/bottom_navigation.dart'; +import 'package:app/widgets/bottom_navigation_item.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:intl/intl.dart'; + +Future showPerson(BuildContext context, {Person? person}) async { + PersonsViewModel vm = PersonsViewModel(); + + final formKey = GlobalKey(); + final firstnameController = TextEditingController(); + final lastnameController = TextEditingController(); + final cityController = TextEditingController(); + final zipController = TextEditingController(); + final streetController = TextEditingController(); + final countryController = TextEditingController(); + final birthdayController = TextEditingController(); + + Future _init() async { + if (person == null) { + person ??= Person(); + // person ??= Person(accountId: await BackendService.accountId); + } else { + firstnameController.text = person!.firstname; + lastnameController.text = person!.lastname; + cityController.text = person!.city; + zipController.text = person!.zip; + streetController.text = person!.street; + countryController.text = person!.country; + birthdayController.text = + DateFormat('dd.MM.yyyy').format(person!.birthday.toDateTime()); + } + } + + await _init(); + + void _updateData() { + person!.firstname = firstnameController.text; + person!.lastname = lastnameController.text; + person!.city = cityController.text; + person!.street = streetController.text; + person!.zip = zipController.text; + person!.country = countryController.text; + } + + Future createPerson(BuildContext context) async { + final navigator = Navigator.of(context); + _updateData(); + person!.id = Int64(0); + person = await vm.createPerson(context, + firstname: person!.firstname, + lastname: person!.lastname, + street: person!.street, + zip: person!.zip, + city: person!.city, + country: person!.country, + birthday: person!.birthday); + + if (person!.id != 0) { + navigator.pop(person); + } + } + + Future updatePerson(BuildContext context) async { + final navigator = Navigator.of(context); + _updateData(); + final personUpdate = await vm.updatePerson(context, + id: person!.id, + firstname: person!.firstname != firstnameController.text + ? person!.firstname + : null, + lastname: person!.lastname != lastnameController.text + ? person!.lastname + : null, + street: person!.street != streetController.text ? person!.street : null, + zip: person!.zip != zipController.text ? person!.zip : null, + city: person!.city != cityController.text ? person!.city : null, + country: + person!.country != countryController.text ? person!.country : null, + birthday: + DateFormat('dd.MM.yyyy').format(person!.birthday.toDateTime()) != + birthdayController.text + ? person!.birthday + : null); + + if (personUpdate != person) { + navigator.pop(person); + } + } + + // ignore: use_build_context_synchronously + await showModalBottomSheet( + context: context, + builder: (builder) { + return Background( + child: Scaffold( + bottomNavigationBar: BottomNavigation( + hideMenu: true, + 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, + ), + Text( + person!.id == 0 ? 'Person anlegen' : 'Person anpassen', + style: const TextStyle( + fontFamily: 'sans-serif', + fontSize: 24, + height: 1.6, + fontWeight: FontWeight.normal, + letterSpacing: 6), + ), + ChangeNotifierProvider( + create: (context) => PersonsViewModel(), + child: Consumer( + builder: (context, value, child) => Form( + key: formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox( + height: 40, + ), + TextFormField( + controller: firstnameController, + decoration: const InputDecoration( + fillColor: Color.fromARGB(30, 255, 255, 255), + filled: true, + suffix: Text('Vorname'), + hintStyle: TextStyle( + color: Colors.white38, + ), + hintText: 'Vorname', + ), + keyboardType: TextInputType.name, + validator: (value) { + if (value == null || !value.isValidName) { + return 'Bitte einen gültigen Vornamen eingeben'; + } + return null; + }, + ), + TextFormField( + controller: lastnameController, + decoration: const InputDecoration( + fillColor: Color.fromARGB(30, 255, 255, 255), + filled: true, + suffix: Text('Nachname'), + hintStyle: TextStyle( + color: Colors.white38, + ), + hintText: 'Nachname', + ), + keyboardType: TextInputType.name, + validator: (value) { + if (value == null || !value.isValidName) { + return 'Bitte einen gültigen Nachnamen eingeben'; + } + return null; + }, + ), + TextFormField( + readOnly: true, + onTap: () async { + DateTime? pickedDate = await showDatePicker( + context: context, + locale: const Locale('de', 'DE'), + initialDate: DateTime.now(), + firstDate: DateTime(1930), + lastDate: DateTime(DateTime.now().year + 1), + builder: (context, child) => Theme( + data: ThemeData.dark(), + child: child != null ? child : Text(''), + ), + ); + + if (pickedDate != null) { + person!.birthday = + Timestamp.fromDateTime(pickedDate); + birthdayController.text = + DateFormat('dd.MM.yyyy') + .format(pickedDate); + } + }, + controller: birthdayController, + decoration: const InputDecoration( + fillColor: Color.fromARGB(30, 255, 255, 255), + filled: true, + suffix: Text('Geburtstag'), + hintStyle: TextStyle( + color: Colors.white38, + ), + hintText: 'Geburtstag', + ), + keyboardType: TextInputType.name, + // validator: (value) { + // if (value == null || !value.isValidName) { + // return 'Bitte einen gültigen Nachnamen eingeben'; + // } + // return null; + // }, + ), + TextFormField( + controller: streetController, + decoration: const InputDecoration( + fillColor: Color.fromARGB(30, 255, 255, 255), + filled: true, + suffix: Text('Straße'), + hintStyle: TextStyle( + color: Colors.white38, + ), + hintText: 'Straße mit Hausnummer', + ), + keyboardType: TextInputType.name, + validator: (value) { + if (value == null || !value.isValidName) { + return 'Bitte eine gültige Straße mit Hausnummer eingeben'; + } + return null; + }, + ), + TextFormField( + controller: zipController, + decoration: const InputDecoration( + fillColor: Color.fromARGB(30, 255, 255, 255), + filled: true, + suffix: Text('PLZ'), + hintStyle: TextStyle( + color: Colors.white38, + ), + hintText: 'PLZ', + ), + keyboardType: TextInputType.name, + validator: (value) { + if (value == null || !value.isValidName) { + return 'Bitte eine gültige PLZ eingeben'; + } + return null; + }, + ), + TextFormField( + controller: cityController, + decoration: const InputDecoration( + fillColor: Color.fromARGB(30, 255, 255, 255), + filled: true, + suffix: Text('Stadt'), + hintStyle: TextStyle( + color: Colors.white38, + ), + hintText: 'Stadt', + ), + keyboardType: TextInputType.name, + validator: (value) { + if (value == null || !value.isValidName) { + return 'Bitte eine gültige Stadt eingeben'; + } + return null; + }, + ), + TextFormField( + controller: countryController, + decoration: const InputDecoration( + fillColor: Color.fromARGB(30, 255, 255, 255), + filled: true, + suffix: Text('Land'), + hintStyle: TextStyle( + color: Colors.white38, + ), + hintText: 'Land', + ), + keyboardType: TextInputType.name, + validator: (value) { + if (value == null || !value.isValidName) { + return 'Bitte ein gültiges Land eingeben'; + } + return null; + }, + ), + const SizedBox( + height: 15, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: () async { + person!.id.isZero + ? await createPerson(context) + : await updatePerson(context); + }, + child: const Icon(Icons.update), + ), + ], + ) + ], + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + useSafeArea: true, + isScrollControlled: true, + backgroundColor: Colors.black); + return person!; +} diff --git a/frontend/app/lib/pages/persons_page.dart b/frontend/app/lib/pages_draft/persons_page.dart similarity index 65% rename from frontend/app/lib/pages/persons_page.dart rename to frontend/app/lib/pages_draft/persons_page.dart index d863dec..1f22ca9 100644 --- a/frontend/app/lib/pages/persons_page.dart +++ b/frontend/app/lib/pages_draft/persons_page.dart @@ -1,8 +1,10 @@ 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/pages_draft/home_page.dart'; +import 'package:app/pages_draft/person_details_page.dart'; import 'package:app/pb/person.pb.dart'; +import 'package:app/util/validation.dart'; import 'package:app/widgets/background.dart'; import 'package:app/widgets/bottom_navigation.dart'; import 'package:app/widgets/bottom_navigation_item.dart'; @@ -22,24 +24,25 @@ class _PersonsPageState extends State { @override void initState() { super.initState(); - _init(); + // _init(); } - void _init() async { - _setLoading(true); - _loggedin = await BackendService.isLoggedIn; - _setLoading(false); - } + // void _init() async { + // _setLoading(true); + // _loggedin = await BackendService.isLoggedIn; + // _setLoading(false); + // } - void _setLoading(bool loading) { - setState(() { - _loading = loading; - }); - } + // void _setLoading(bool loading) { + // setState(() { + // _loading = loading; + // }); + // } void _checkResponse(ApiResponse response) { if (response.status == Status.ERROR && - response.message!.contains('unauthenticated')) { + (response.message!.contains('unauthenticated') || + response.message!.contains('blocked'))) { BackendService.logout(); Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( @@ -50,7 +53,7 @@ class _PersonsPageState extends State { } } - bool _loading = true; + bool _loading = false; bool _loggedin = false; List persons = []; @@ -61,13 +64,58 @@ class _PersonsPageState extends State { } List _personsList(List persons) { + persons.sort((a, b) { + final comp = a.lastname.compareTo(b.lastname); + if (comp != 0) { + return comp; + } + return a.firstname.compareTo(b.firstname); + }); 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), + list.add(TextButton( + onPressed: () async { + final Person per = await showPerson(context, person: p); + if (!per.id.isZero && !persons.contains(per)) { + setState(() { + this.persons.add(per); + }); + } + }, + child: Card( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + color: const Color.fromARGB(100, 89, 88, 88), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 14), + child: Row( + children: [ + SizedBox( + height: 40, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + p.lastname.titleCase, + style: const TextStyle(color: Colors.white), + // overflow: TextOverflow.fade, + textAlign: TextAlign.start, + ), + const Spacer(), + Text( + p.firstname.titleCase, + style: const TextStyle(color: Colors.white), + textAlign: TextAlign.start, + // overflow: TextOverflow.fade, + ), + ], + ), + ), + const Spacer(), + const Text('STATUS') + ], + ), + ), ), )); } @@ -80,7 +128,14 @@ class _PersonsPageState extends State { child: Scaffold( floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: FloatingActionButton( - onPressed: () {}, + onPressed: () async { + final p = await showPerson(context); + if (!p.id.isZero && !persons.contains(p)) { + setState(() { + persons.add(p); + }); + } + }, child: const Icon(Icons.add), ), appBar: AppBar( @@ -163,7 +218,9 @@ class _PersonsPageState extends State { child: Consumer( builder: (context, value, child) { _checkResponse(value.response); - listPersons(context); + if (persons.isEmpty) { + listPersons(context); + } return _loading ? const CircularProgressIndicator( color: Colors.grey, diff --git a/frontend/app/lib/pages_old/dashboard_page.dart b/frontend/app/lib/pages_old/dashboard_page.dart index 3c8503b..e012989 100644 --- a/frontend/app/lib/pages_old/dashboard_page.dart +++ b/frontend/app/lib/pages_old/dashboard_page.dart @@ -1,9 +1,5 @@ -import 'package:app/gapi/client.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'; -import 'package:app/widgets/bottom_bar.dart'; import 'package:app/widgets/loading_widget.dart'; import 'package:app/widgets/side_drawer.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/app/lib/pages_old/login_page.dart b/frontend/app/lib/pages_old/login_page.dart index d2207e7..43923c2 100644 --- a/frontend/app/lib/pages_old/login_page.dart +++ b/frontend/app/lib/pages_old/login_page.dart @@ -1,12 +1,9 @@ -import 'package:app/gapi/client.dart'; import 'package:app/model/services/backend_service.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'; import 'package:app/widgets/side_drawer.dart'; import 'package:flutter/material.dart'; -import 'package:grpc/grpc.dart'; // GlobalKey scaffoldKey = GlobalKey(); diff --git a/frontend/app/lib/pages_old/register_page.dart b/frontend/app/lib/pages_old/register_page.dart index 2a40fd6..5532a09 100644 --- a/frontend/app/lib/pages_old/register_page.dart +++ b/frontend/app/lib/pages_old/register_page.dart @@ -1,11 +1,8 @@ -import 'package:app/gapi/client.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'; import 'package:app/widgets/side_drawer.dart'; import 'package:flutter/material.dart'; -import 'package:grpc/grpc.dart'; class RegisterPage extends StatefulWidget { const RegisterPage({ diff --git a/frontend/app/lib/pages_old/start_page.dart b/frontend/app/lib/pages_old/start_page.dart index 6235c2c..4eb160f 100644 --- a/frontend/app/lib/pages_old/start_page.dart +++ b/frontend/app/lib/pages_old/start_page.dart @@ -1,9 +1,6 @@ -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_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/util/colors.dart b/frontend/app/lib/util/colors.dart index 5c8d297..e7a4484 100644 --- a/frontend/app/lib/util/colors.dart +++ b/frontend/app/lib/util/colors.dart @@ -2,10 +2,18 @@ import 'package:flutter/material.dart'; class CustomColors { static Color get error { - return const Color.fromARGB(200, 255, 90, 90); + return const Color.fromARGB(255, 255, 82, 82); } static Color get success { - return const Color.fromARGB(200, 55, 125, 55); + return const Color.fromARGB(255, 51, 217, 178); + } + + static Color get primary { + return const Color.fromARGB(255, 51, 217, 178); + } + + static Color get secondary { + return const Color.fromARGB(255, 52, 172, 224); } } diff --git a/frontend/app/lib/util/enums.dart b/frontend/app/lib/util/enums.dart new file mode 100644 index 0000000..e69de29 diff --git a/frontend/app/lib/util/validation.dart b/frontend/app/lib/util/validation.dart index a7aa9e5..10f779c 100644 --- a/frontend/app/lib/util/validation.dart +++ b/frontend/app/lib/util/validation.dart @@ -2,7 +2,7 @@ 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'^.+$'); +final passwordRegExp = RegExp(r'^[0-9a-zA-Z\-\_\.\,\*\+\=?!]{12,64}$'); extension valString on String { bool get isValidEmail { @@ -24,4 +24,10 @@ extension valString on String { bool get isValidPhone { return phoneRegExp.hasMatch(this); } + + String get titleCase { + return split(' ') + .map((str) => str[0].toUpperCase() + str.substring(1)) + .join(' '); + } } diff --git a/frontend/app/lib/widgets/bottom_navigation.dart b/frontend/app/lib/widgets/bottom_navigation.dart index c3f6519..9f8b0d7 100644 --- a/frontend/app/lib/widgets/bottom_navigation.dart +++ b/frontend/app/lib/widgets/bottom_navigation.dart @@ -7,10 +7,13 @@ class BottomNavigation extends StatelessWidget { required this.children, this.backgroundColor, this.iconColor, + this.hideMenu, }) { + hideMenu ??= false; backgroundColor ??= Colors.black; } + bool? hideMenu; List children; Color? backgroundColor; Color? iconColor; @@ -32,14 +35,18 @@ class BottomNavigation extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ ...children, - Builder(builder: (context) { - return IconButton( - onPressed: () => Scaffold.of(context).openDrawer(), - icon: const Icon( - Icons.menu, - color: Colors.white, - )); - }), + if (!hideMenu!) + Builder( + builder: (context) { + return IconButton( + onPressed: () => Scaffold.of(context).openDrawer(), + icon: const Icon( + Icons.menu, + color: Colors.white, + ), + ); + }, + ) ], ), ), diff --git a/frontend/app/linux/flutter/generated_plugin_registrant.cc b/frontend/app/linux/flutter/generated_plugin_registrant.cc index e71a16d..d0e7f79 100644 --- a/frontend/app/linux/flutter/generated_plugin_registrant.cc +++ b/frontend/app/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); } diff --git a/frontend/app/linux/flutter/generated_plugins.cmake b/frontend/app/linux/flutter/generated_plugins.cmake index 2e1de87..b29e9ba 100644 --- a/frontend/app/linux/flutter/generated_plugins.cmake +++ b/frontend/app/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/frontend/app/pubspec.lock b/frontend/app/pubspec.lock index 112b12d..e313442 100644 --- a/frontend/app/pubspec.lock +++ b/frontend/app/pubspec.lock @@ -89,6 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" fixnum: dependency: "direct main" description: @@ -110,11 +118,77 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + url: "https://pub.dev" + source: hosted + version: "2.0.17" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685 + url: "https://pub.dev" + source: hosted + version: "9.0.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c + url: "https://pub.dev" + source: hosted + version: "3.0.1" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108" + url: "https://pub.dev" + source: hosted + version: "3.0.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" googleapis_auth: dependency: transitive description: @@ -155,6 +229,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" js: dependency: transitive description: @@ -171,6 +253,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + local_auth: + dependency: "direct main" + description: + name: local_auth + sha256: "7e6c63082e399b61e4af71266b012e767a5d4525dd6e9ba41e174fd42d76e115" + url: "https://pub.dev" + source: hosted + version: "2.1.7" + local_auth_android: + dependency: transitive + description: + name: local_auth_android + sha256: df4ccb3193525b8a60c78a5ca7bf188a47705bcf77bcc837a6b2cf6da64ae0e2 + url: "https://pub.dev" + source: hosted + version: "1.0.35" + local_auth_ios: + dependency: transitive + description: + name: local_auth_ios + sha256: "8293faf72ef0ac4710f209edd03916c2d4c1eeab0483bdcf9b2e659c2f7d737b" + url: "https://pub.dev" + source: hosted + version: "1.1.5" + local_auth_platform_interface: + dependency: transitive + description: + name: local_auth_platform_interface + sha256: fc5bd537970a324260fda506cfb61b33ad7426f37a8ea5c461cf612161ebba54 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + local_auth_windows: + dependency: transitive + description: + name: local_auth_windows + sha256: "505ba3367ca781efb1c50d3132e44a2446bccc4163427bc203b9b4d8994d97ea" + url: "https://pub.dev" + source: hosted + version: "1.0.10" matcher: dependency: transitive description: @@ -211,6 +333,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.3" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + url: "https://pub.dev" + source: hosted + version: "2.1.6" pointycastle: dependency: transitive description: @@ -336,6 +522,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.4-beta" + win32: + dependency: transitive + description: + name: win32 + sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + url: "https://pub.dev" + source: hosted + version: "5.0.9" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + url: "https://pub.dev" + source: hosted + version: "1.0.3" sdks: dart: ">=3.1.4 <4.0.0" - flutter: ">=3.3.0" + flutter: ">=3.10.0" diff --git a/frontend/app/pubspec.yaml b/frontend/app/pubspec.yaml index 2960757..7b5cda3 100644 --- a/frontend/app/pubspec.yaml +++ b/frontend/app/pubspec.yaml @@ -28,11 +28,14 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + local_auth: ^2.1.7 async: ^2.2.0 protobuf: ^3.0.0 collection: ^1.15.0-nullsafety.4 flutter: sdk: flutter + flutter_localizations: + sdk: flutter # The following adds the Cupertino Icons font to your application. @@ -43,6 +46,8 @@ dependencies: path: ^1.8.3 fixnum: ^1.1.0 provider: ^6.0.5 + intl: ^0.18.1 + flutter_secure_storage: ^9.0.0 dev_dependencies: lints: ^2.0.0 @@ -69,6 +74,8 @@ flutter: # To add assets to your application, add an assets section, like this: assets: + - assets/chat_bubbles.png + - assets/JPEG.jpg - lib/assets/logo_300x200.png - lib/assets/hero-pattern-300x200.png # - images/a_dot_burr.jpeg diff --git a/frontend/app/windows/flutter/generated_plugin_registrant.cc b/frontend/app/windows/flutter/generated_plugin_registrant.cc index 8b6d468..011734d 100644 --- a/frontend/app/windows/flutter/generated_plugin_registrant.cc +++ b/frontend/app/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,12 @@ #include "generated_plugin_registrant.h" +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + LocalAuthPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("LocalAuthPlugin")); } diff --git a/frontend/app/windows/flutter/generated_plugins.cmake b/frontend/app/windows/flutter/generated_plugins.cmake index b93c4c3..11485fc 100644 --- a/frontend/app/windows/flutter/generated_plugins.cmake +++ b/frontend/app/windows/flutter/generated_plugins.cmake @@ -3,6 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_windows + local_auth_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST