ft/adds account_info and late_person pages
This commit is contained in:
parent
cc6841448d
commit
ebe3426c2c
BIN
frontend/app/assets/icons/icon.jpg
Normal file
BIN
frontend/app/assets/icons/icon.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
@ -1,6 +1,8 @@
|
|||||||
import 'package:app/model/services/auth_service.dart';
|
import 'package:app/model/services/auth_service.dart';
|
||||||
import 'package:app/model/services/storage_service.dart';
|
import 'package:app/model/services/storage_service.dart';
|
||||||
import 'package:app/model/view_model/base_vm.dart';
|
import 'package:app/model/view_model/base_vm.dart';
|
||||||
|
import 'package:app/pages/account_info_page.dart';
|
||||||
|
import 'package:app/pages/late_person_page.dart';
|
||||||
import 'package:app/pages/notifications_page.dart';
|
import 'package:app/pages/notifications_page.dart';
|
||||||
import 'package:app/pages/registration_page.dart';
|
import 'package:app/pages/registration_page.dart';
|
||||||
import 'package:app/pages/security_page.dart';
|
import 'package:app/pages/security_page.dart';
|
||||||
@ -110,30 +112,30 @@ class _DigitalerFriedenState extends State<DigitalerFrieden> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_loading) {
|
if (_loading) {
|
||||||
return SafeArea(
|
return const StartPage();
|
||||||
child: Center(
|
// return SafeArea(
|
||||||
child: Column(
|
// child: Center(
|
||||||
children: [
|
// child: Column(
|
||||||
const SizedBox(
|
// mainAxisAlignment: MainAxisAlignment.center,
|
||||||
height: 150,
|
// children: [
|
||||||
),
|
// Hero(
|
||||||
Hero(
|
// tag: 'logo',
|
||||||
tag: 'logo',
|
// child: Image.asset(
|
||||||
child: Image.asset(
|
// 'assets/JPEG.jpg',
|
||||||
'assets/JPEG.jpg',
|
// height: 180,
|
||||||
height: 180,
|
// ),
|
||||||
),
|
// ),
|
||||||
),
|
// ],
|
||||||
CircularProgressIndicator(
|
// ),
|
||||||
color: CustomColors.primary,
|
// ),
|
||||||
),
|
// );
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (verified) {
|
if (verified) {
|
||||||
switch (accountLevel) {
|
switch (accountLevel) {
|
||||||
|
case 6:
|
||||||
|
return const LatePersonPage();
|
||||||
|
case 4 || 5:
|
||||||
|
return const AccountInfoPage();
|
||||||
default:
|
default:
|
||||||
return const StartPage();
|
return const StartPage();
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import 'package:app/pb/google/protobuf/timestamp.pb.dart';
|
|||||||
import 'package:app/pb/person.pb.dart';
|
import 'package:app/pb/person.pb.dart';
|
||||||
import 'package:app/data/database.dart';
|
import 'package:app/data/database.dart';
|
||||||
import 'package:app/pb/rpc_create_account.pb.dart';
|
import 'package:app/pb/rpc_create_account.pb.dart';
|
||||||
|
import 'package:app/pb/rpc_create_account_info.pb.dart';
|
||||||
import 'package:app/pb/rpc_create_person.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.pb.dart';
|
||||||
import 'package:app/pb/rpc_get_account_info.pb.dart';
|
import 'package:app/pb/rpc_get_account_info.pb.dart';
|
||||||
@ -143,15 +144,60 @@ class BackendService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> createAccountInfo(
|
||||||
|
{required String firstname,
|
||||||
|
required String lastname,
|
||||||
|
required String streetAddress,
|
||||||
|
required String zip,
|
||||||
|
required String city,
|
||||||
|
required String country,
|
||||||
|
required String phoneNumber,
|
||||||
|
required DateTime birthday}) async {
|
||||||
|
try {
|
||||||
|
final acc = await account;
|
||||||
|
if (acc == null) {
|
||||||
|
throw FetchDataException('AccountID nicht gespeichert');
|
||||||
|
}
|
||||||
|
final resp = BackendService.client.createAccountInfo(
|
||||||
|
CreateAccountInfoRequest(
|
||||||
|
accountId: acc.id,
|
||||||
|
firstname: firstname,
|
||||||
|
lastname: lastname,
|
||||||
|
street: streetAddress,
|
||||||
|
zip: zip,
|
||||||
|
city: city,
|
||||||
|
country: country,
|
||||||
|
phone: phoneNumber,
|
||||||
|
birthday: Timestamp.fromDateTime(birthday),
|
||||||
|
),
|
||||||
|
options: CallOptions(
|
||||||
|
metadata: {
|
||||||
|
'Authorization': 'Bearer ${await _storageService.accessToken}'
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return resp != null;
|
||||||
|
} on SocketException {
|
||||||
|
throw FetchDataException('Keine Internet Verbindung');
|
||||||
|
} on GrpcError catch (err) {
|
||||||
|
throw FetchDataException('${err.message}');
|
||||||
|
} catch (err) {
|
||||||
|
throw InternalException(err.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> resendVerification({required Int64 accountId}) async {
|
Future<bool> resendVerification({required Int64 accountId}) async {
|
||||||
try {
|
try {
|
||||||
final resp = await BackendService.client.resendVerification(
|
final resp = await BackendService.client.resendVerification(
|
||||||
ResendVerificationRequest(
|
ResendVerificationRequest(
|
||||||
accountId: accountId,
|
accountId: accountId,
|
||||||
),
|
),
|
||||||
options: CallOptions(metadata: {
|
options: CallOptions(
|
||||||
|
metadata: {
|
||||||
'Authorization': 'Bearer ${await _storageService.accessToken}'
|
'Authorization': 'Bearer ${await _storageService.accessToken}'
|
||||||
}));
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
return resp.account.id == accountId;
|
return resp.account.id == accountId;
|
||||||
} on SocketException {
|
} on SocketException {
|
||||||
throw FetchDataException('Keine Internet Verbindung');
|
throw FetchDataException('Keine Internet Verbindung');
|
||||||
|
@ -149,6 +149,67 @@ class BaseViewModel with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> createAccountInfo(
|
||||||
|
BuildContext context, {
|
||||||
|
required String firstname,
|
||||||
|
required String lastname,
|
||||||
|
required String streetAddress,
|
||||||
|
required String zip,
|
||||||
|
required String city,
|
||||||
|
required String country,
|
||||||
|
required String phoneNumber,
|
||||||
|
required DateTime birthday,
|
||||||
|
}) async {
|
||||||
|
notifyListeners();
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
bool resp = false;
|
||||||
|
try {
|
||||||
|
resp = await _service.createAccountInfo(
|
||||||
|
firstname: firstname,
|
||||||
|
lastname: lastname,
|
||||||
|
streetAddress: streetAddress,
|
||||||
|
zip: zip,
|
||||||
|
city: city,
|
||||||
|
country: country,
|
||||||
|
phoneNumber: phoneNumber,
|
||||||
|
birthday: birthday,
|
||||||
|
);
|
||||||
|
if (resp) {
|
||||||
|
_apiResponse = ApiResponse.completed('Registrierung abgeschlossen');
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
} catch (e) {
|
||||||
|
messenger.showSnackBar(SnackBar(
|
||||||
|
backgroundColor: CustomColors.error,
|
||||||
|
content: const Text(
|
||||||
|
'Daten konnten nicht übertragen werden',
|
||||||
|
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();
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
_apiResponse = ApiResponse.loading('Logge aus');
|
_apiResponse = ApiResponse.loading('Logge aus');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
359
frontend/app/lib/pages/account_info_page.dart
Normal file
359
frontend/app/lib/pages/account_info_page.dart
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
import 'package:app/model/services/storage_service.dart';
|
||||||
|
import 'package:app/model/view_model/base_vm.dart';
|
||||||
|
import 'package:app/pages/late_person_page.dart';
|
||||||
|
import 'package:app/pages/loading_page.dart';
|
||||||
|
import 'package:app/pages/notifications_page.dart';
|
||||||
|
import 'package:app/util/colors.dart';
|
||||||
|
import 'package:app/util/validation.dart';
|
||||||
|
import 'package:app/widgets/custom_scaffold.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AccountInfoPage extends StatefulWidget {
|
||||||
|
const AccountInfoPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AccountInfoPage> createState() => _AccountInfoPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AccountInfoPageState extends State<AccountInfoPage> {
|
||||||
|
bool _loading = true;
|
||||||
|
final StorageService _storageService = StorageService();
|
||||||
|
final BaseViewModel _vm = BaseViewModel();
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _firstNameController = TextEditingController();
|
||||||
|
final _lastNameController = TextEditingController();
|
||||||
|
final _streetController = TextEditingController();
|
||||||
|
final _houseNumberController = TextEditingController();
|
||||||
|
final _zipController = TextEditingController();
|
||||||
|
final _cityController = TextEditingController();
|
||||||
|
final _phoneController = TextEditingController();
|
||||||
|
final _countryController = TextEditingController();
|
||||||
|
final _birthdayController = TextEditingController();
|
||||||
|
final _birthPlaceController = TextEditingController();
|
||||||
|
|
||||||
|
DateTime? _birthday;
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_init();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_firstNameController.dispose();
|
||||||
|
_lastNameController.dispose();
|
||||||
|
_streetController.dispose();
|
||||||
|
_houseNumberController.dispose();
|
||||||
|
_zipController.dispose();
|
||||||
|
_cityController.dispose();
|
||||||
|
_phoneController.dispose();
|
||||||
|
_countryController.dispose();
|
||||||
|
_birthPlaceController.dispose();
|
||||||
|
_birthdayController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _init() async {
|
||||||
|
_countryController.text = 'Deutschland';
|
||||||
|
if (await _storageService.accountLevel < 5) {
|
||||||
|
await _storageService.setAccountLevel(5);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _selectDate(BuildContext context) async {
|
||||||
|
final DateTime? picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
builder: (context, child) => Theme(
|
||||||
|
data: ThemeData.dark(),
|
||||||
|
child: child ?? const Text(''),
|
||||||
|
),
|
||||||
|
initialDate: DateTime.now().add(const Duration(days: 365 * -18)),
|
||||||
|
firstDate: DateTime.now().add(const Duration(days: 365 * -100)),
|
||||||
|
lastDate: DateTime.now().add(const Duration(days: 365 * -18)),
|
||||||
|
);
|
||||||
|
// firstDate: DateTime.now().add(const Duration(days: 365 * -100)),
|
||||||
|
// lastDate: DateTime.now().add(const Duration(days: 365 * -18)));
|
||||||
|
if (picked != null && picked != _birthday) {
|
||||||
|
setState(() {
|
||||||
|
_birthday = picked;
|
||||||
|
_birthdayController.text =
|
||||||
|
'${picked.day.toString().padLeft(2, '0')}.${picked.month.toString().padLeft(2, '0')}.${picked.year}';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.height,
|
||||||
|
child: CustomScaffold(
|
||||||
|
backButton: BackButton(
|
||||||
|
color: CustomColors.primary,
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (builder) => const NotificationsPage()));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
children: _loading
|
||||||
|
? [
|
||||||
|
LoadingPage(),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
const Text(
|
||||||
|
'Registrierung abschließen',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'sans-serif',
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 2.0,
|
||||||
|
fontSize: 25,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height:
|
||||||
|
MediaQuery.of(context).viewInsets.bottom > 0 ? 230 : 460,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: _firstNameController,
|
||||||
|
autocorrect: false,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
label: Text('Vorname'),
|
||||||
|
filled: true,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.name,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || !value.isValidName) {
|
||||||
|
return 'Bitte einen gültigen Vornamen eingeben';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
TextFormField(
|
||||||
|
controller: _lastNameController,
|
||||||
|
autocorrect: false,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
label: Text('Nachname'),
|
||||||
|
filled: true,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.name,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || !value.isValidName) {
|
||||||
|
return 'Bitte einen gültigen Nachnamen eingeben';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
TextFormField(
|
||||||
|
controller: _streetController,
|
||||||
|
autocorrect: false,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
label: Text('Straße'),
|
||||||
|
filled: true,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.streetAddress,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null ||
|
||||||
|
!value.isValidStreetAddress) {
|
||||||
|
return 'Bitte eine gültige Straße eingeben';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 130,
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _houseNumberController,
|
||||||
|
autocorrect: false,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
label: Text('Hausnummer'),
|
||||||
|
filled: true,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null ||
|
||||||
|
!value.isValidHouseNumber) {
|
||||||
|
return 'Nur Zahlen erlaubt';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
SizedBox(
|
||||||
|
width: 160,
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _zipController,
|
||||||
|
autocorrect: false,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
label: Text('Postleitzahl'),
|
||||||
|
filled: true,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null ||
|
||||||
|
!value.isValidZip) {
|
||||||
|
return 'Bitte eine gültige Postleitzahl eingeben';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
TextFormField(
|
||||||
|
controller: _cityController,
|
||||||
|
autocorrect: false,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
label: Text('Stadt'),
|
||||||
|
filled: true,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.name,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || !value.isValidCity) {
|
||||||
|
return 'Bitte einen gültigen Ort eingeben';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
TextFormField(
|
||||||
|
controller: _countryController,
|
||||||
|
autocorrect: false,
|
||||||
|
autofocus: true,
|
||||||
|
readOnly: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
label: Text('Land'),
|
||||||
|
filled: true,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.name,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || !value.isValidName) {
|
||||||
|
return 'Bitte einen gültigen Ort eingeben';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
TextFormField(
|
||||||
|
controller: _phoneController,
|
||||||
|
autocorrect: false,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
label: Text('Telefonnummer'),
|
||||||
|
filled: true,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || !value.isValidPhone) {
|
||||||
|
return 'Bitte eine gültige Telefonnummer eingeben';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
TextFormField(
|
||||||
|
onTap: () => _selectDate(context),
|
||||||
|
controller: _birthdayController,
|
||||||
|
autocorrect: false,
|
||||||
|
autofocus: true,
|
||||||
|
readOnly: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
label: Text('Geburtsdatum'),
|
||||||
|
filled: true,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.datetime,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Bitte ein gültiges Datum eingeben';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Hero(
|
||||||
|
tag: 'flow-button',
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: CustomColors.primary,
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
final resp = await _vm.createAccountInfo(
|
||||||
|
context,
|
||||||
|
firstname: _firstNameController.text,
|
||||||
|
lastname: _lastNameController.text,
|
||||||
|
city: _cityController.text,
|
||||||
|
country: _countryController.text,
|
||||||
|
zip: _zipController.text,
|
||||||
|
phoneNumber: _phoneController.text,
|
||||||
|
streetAddress: _streetController.text,
|
||||||
|
birthday: _birthday!,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (resp) {
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (builder) => const LatePersonPage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const SizedBox(
|
||||||
|
height: 60,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Weiter',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
64
frontend/app/lib/pages/late_person_page.dart
Normal file
64
frontend/app/lib/pages/late_person_page.dart
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import 'package:app/model/services/storage_service.dart';
|
||||||
|
import 'package:app/pages/loading_page.dart';
|
||||||
|
import 'package:app/pages/notifications_page.dart';
|
||||||
|
import 'package:app/util/colors.dart';
|
||||||
|
import 'package:app/widgets/custom_scaffold.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class LatePersonPage extends StatefulWidget {
|
||||||
|
const LatePersonPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LatePersonPage> createState() => _LatePersonPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LatePersonPageState extends State<LatePersonPage> {
|
||||||
|
final StorageService _storageService = StorageService();
|
||||||
|
bool _loading = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_init();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _init() async {
|
||||||
|
if (await _storageService.accountLevel < 6) {
|
||||||
|
await _storageService.setAccountLevel(6);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CustomScaffold(
|
||||||
|
backButton: BackButton(
|
||||||
|
color: CustomColors.primary,
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (builder) => const NotificationsPage()));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
children: _loading
|
||||||
|
? [
|
||||||
|
LoadingPage(),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
const Text(
|
||||||
|
'Daten der Verstorbenen',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'sans-serif',
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 2.0,
|
||||||
|
fontSize: 25,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
18
frontend/app/lib/pages/loading_page.dart
Normal file
18
frontend/app/lib/pages/loading_page.dart
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class LoadingPage extends StatelessWidget {
|
||||||
|
LoadingPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Hero(
|
||||||
|
tag: 'logo',
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/icons/icon.jpg',
|
||||||
|
// height: 180,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
import 'package:app/model/services/storage_service.dart';
|
import 'package:app/model/services/storage_service.dart';
|
||||||
|
import 'package:app/pages/account_info_page.dart';
|
||||||
|
import 'package:app/pages/late_person_page.dart';
|
||||||
import 'package:app/pages/registration_page.dart';
|
import 'package:app/pages/registration_page.dart';
|
||||||
import 'package:app/pages/security_page.dart';
|
import 'package:app/pages/security_page.dart';
|
||||||
import 'package:app/pages/verify_email_page.dart';
|
import 'package:app/pages/verify_email_page.dart';
|
||||||
@ -123,13 +125,39 @@ class _NotificationsPageState extends State<NotificationsPage> {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await _setNotificationSetting(true);
|
await _setNotificationSetting(true);
|
||||||
if (await _storageService.accessToken != null) {
|
if (await _storageService.accessToken != null) {
|
||||||
if (mounted) {
|
if (await _storageService.verified) {
|
||||||
Navigator.push(
|
switch (await _storageService.accountLevel) {
|
||||||
context,
|
case 4 || 5:
|
||||||
MaterialPageRoute(
|
if (mounted) {
|
||||||
builder: (builder) => const VerifyEmailPage(),
|
Navigator.push(
|
||||||
),
|
context,
|
||||||
);
|
MaterialPageRoute(
|
||||||
|
builder: (builder) =>
|
||||||
|
const AccountInfoPage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 6:
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (builder) =>
|
||||||
|
const LatePersonPage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (builder) =>
|
||||||
|
const VerifyEmailPage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@ -171,14 +199,38 @@ class _NotificationsPageState extends State<NotificationsPage> {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await _setNotificationSetting(false);
|
await _setNotificationSetting(false);
|
||||||
if (await _storageService.accessToken != null) {
|
if (await _storageService.accessToken != null) {
|
||||||
if (mounted) {
|
if (await _storageService.verified) {
|
||||||
Navigator.push(
|
switch (await _storageService.accountLevel) {
|
||||||
context,
|
case 4 || 5:
|
||||||
MaterialPageRoute(
|
if (mounted) {
|
||||||
builder: (builder) => const VerifyEmailPage(),
|
Navigator.push(
|
||||||
// builder: (builder) => SecurityPage(),
|
context,
|
||||||
),
|
MaterialPageRoute(
|
||||||
);
|
builder: (builder) =>
|
||||||
|
const AccountInfoPage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 6:
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (builder) =>
|
||||||
|
const LatePersonPage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (builder) => const VerifyEmailPage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@ -186,7 +238,6 @@ class _NotificationsPageState extends State<NotificationsPage> {
|
|||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (builder) => const RegistrationPage(),
|
builder: (builder) => const RegistrationPage(),
|
||||||
// builder: (builder) => SecurityPage(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:app/model/view_model/base_vm.dart';
|
import 'package:app/model/view_model/base_vm.dart';
|
||||||
|
import 'package:app/pages/account_info_page.dart';
|
||||||
import 'package:app/pages/verify_email_page.dart';
|
import 'package:app/pages/verify_email_page.dart';
|
||||||
import 'package:app/util/colors.dart';
|
import 'package:app/util/colors.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -201,7 +202,17 @@ class _PasswordPageState extends State<PasswordPage> {
|
|||||||
password: _passwordController1.text,
|
password: _passwordController1.text,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (loggedin && mounted) {
|
if (loggedin) {
|
||||||
|
FocusManager.instance.primaryFocus
|
||||||
|
?.unfocus();
|
||||||
|
final acc = await _vm.account;
|
||||||
|
if (acc!.emailVerified) {
|
||||||
|
navigator.push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (builder) =>
|
||||||
|
const AccountInfoPage()),
|
||||||
|
);
|
||||||
|
}
|
||||||
navigator.push(
|
navigator.push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (builder) =>
|
builder: (builder) =>
|
||||||
@ -220,7 +231,7 @@ class _PasswordPageState extends State<PasswordPage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
widget.register
|
widget.register
|
||||||
? 'Registrierung abschließen'
|
? 'Konto erstellen'
|
||||||
: 'Einloggen',
|
: 'Einloggen',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import 'package:app/model/services/storage_service.dart';
|
import 'package:app/model/services/storage_service.dart';
|
||||||
|
import 'package:app/pages/loading_page.dart';
|
||||||
import 'package:app/pages/notifications_page.dart';
|
import 'package:app/pages/notifications_page.dart';
|
||||||
import 'package:app/pages/password_page.dart';
|
import 'package:app/pages/password_page.dart';
|
||||||
import 'package:app/util/colors.dart';
|
import 'package:app/util/colors.dart';
|
||||||
import 'package:app/util/validation.dart';
|
import 'package:app/util/validation.dart';
|
||||||
|
import 'package:app/widgets/custom_scaffold.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class RegistrationPage extends StatefulWidget {
|
class RegistrationPage extends StatefulWidget {
|
||||||
@ -42,169 +44,140 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SafeArea(
|
return CustomScaffold(
|
||||||
child: _loading
|
backButton: BackButton(
|
||||||
? Center(
|
color: CustomColors.primary,
|
||||||
child: Column(
|
onPressed: () {
|
||||||
children: [
|
Navigator.pushAndRemoveUntil(
|
||||||
const SizedBox(
|
context,
|
||||||
height: 150,
|
MaterialPageRoute(
|
||||||
),
|
builder: (builder) => const NotificationsPage()),
|
||||||
Hero(
|
(route) => false);
|
||||||
tag: 'logo',
|
},
|
||||||
child: Image.asset(
|
),
|
||||||
'assets/JPEG.jpg',
|
children: _loading
|
||||||
height: 180,
|
? [
|
||||||
),
|
LoadingPage(),
|
||||||
),
|
]
|
||||||
CircularProgressIndicator(
|
: [
|
||||||
color: CustomColors.primary,
|
const SizedBox(
|
||||||
),
|
height: 20,
|
||||||
],
|
|
||||||
),
|
),
|
||||||
)
|
const Text(
|
||||||
: Scaffold(
|
'Jetzt Registrieren',
|
||||||
appBar: AppBar(
|
textAlign: TextAlign.center,
|
||||||
leading: BackButton(
|
style: TextStyle(
|
||||||
color: CustomColors.primary,
|
fontFamily: 'sans-serif',
|
||||||
onPressed: () {
|
fontWeight: FontWeight.bold,
|
||||||
Navigator.pushAndRemoveUntil(
|
letterSpacing: 2.0,
|
||||||
context,
|
fontSize: 25,
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (builder) => const NotificationsPage()),
|
|
||||||
(route) => false);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
iconTheme: IconThemeData(
|
|
||||||
color: CustomColors.primary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Padding(
|
const SizedBox(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
|
height: 20,
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'Gib deine E-Mail Adresse ein.',
|
||||||
|
// textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
),
|
||||||
|
Form(
|
||||||
|
key: formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
// mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
TextFormField(
|
||||||
height: 20,
|
autocorrect: false,
|
||||||
),
|
autofocus: true,
|
||||||
const Text(
|
controller: mailController,
|
||||||
'Jetzt Registrieren',
|
keyboardType: TextInputType.emailAddress,
|
||||||
textAlign: TextAlign.center,
|
decoration: const InputDecoration(
|
||||||
style: TextStyle(
|
helperText: 'test',
|
||||||
fontFamily: 'sans-serif',
|
label: Text('E-Mail Adresse'),
|
||||||
fontWeight: FontWeight.bold,
|
filled: true,
|
||||||
letterSpacing: 2.0,
|
|
||||||
fontSize: 25,
|
|
||||||
),
|
),
|
||||||
),
|
validator: (value) {
|
||||||
const SizedBox(
|
if (value == null || !value.isValidEmail) {
|
||||||
height: 20,
|
return 'Bitte eine valide E-Mail Adresse angeben';
|
||||||
),
|
} else {
|
||||||
const Text(
|
return null;
|
||||||
'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,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import 'package:app/model/services/auth_service.dart';
|
import 'package:app/model/services/auth_service.dart';
|
||||||
import 'package:app/model/services/storage_service.dart';
|
import 'package:app/model/services/storage_service.dart';
|
||||||
|
import 'package:app/pages/loading_page.dart';
|
||||||
import 'package:app/pages/notifications_page.dart';
|
import 'package:app/pages/notifications_page.dart';
|
||||||
import 'package:app/pages/start_page.dart';
|
import 'package:app/pages/start_page.dart';
|
||||||
import 'package:app/util/colors.dart';
|
import 'package:app/util/colors.dart';
|
||||||
|
import 'package:app/widgets/custom_scaffold.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SecurityPage extends StatefulWidget {
|
class SecurityPage extends StatefulWidget {
|
||||||
@ -33,151 +35,130 @@ class _SecurityPageState extends State<SecurityPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SafeArea(
|
return CustomScaffold(
|
||||||
child: _loading
|
backButton: BackButton(
|
||||||
? Center(
|
color: CustomColors.primary,
|
||||||
child: Column(
|
onPressed: () {
|
||||||
children: [
|
Navigator.push(
|
||||||
const SizedBox(
|
context,
|
||||||
height: 150,
|
MaterialPageRoute(builder: (builder) => const StartPage()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
children: _loading
|
||||||
|
? [
|
||||||
|
LoadingPage(),
|
||||||
|
// Center(
|
||||||
|
// child: Hero(
|
||||||
|
// tag: 'logo',
|
||||||
|
// child: Image.asset(
|
||||||
|
// 'assets/JPEG.jpg',
|
||||||
|
// height: 180,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
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,
|
||||||
),
|
),
|
||||||
Hero(
|
onPressed: () async {
|
||||||
tag: 'logo',
|
bool isAuthenticated =
|
||||||
child: Image.asset(
|
await AuthService.authenticateWithBiometrics();
|
||||||
'assets/JPEG.jpg',
|
if (isAuthenticated) {
|
||||||
height: 180,
|
if (mounted) {
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
CircularProgressIndicator(
|
|
||||||
color: CustomColors.primary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
leading: BackButton(
|
|
||||||
color: CustomColors.primary,
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pushAndRemoveUntil(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (builder) => const StartPage()),
|
|
||||||
(route) => false);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
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) {
|
|
||||||
if (mounted) {
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
// 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,
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:app/pages/agb_page.dart';
|
import 'package:app/pages/agb_page.dart';
|
||||||
import 'package:app/pages/security_page.dart';
|
import 'package:app/pages/security_page.dart';
|
||||||
import 'package:app/util/colors.dart';
|
import 'package:app/util/colors.dart';
|
||||||
|
import 'package:app/widgets/custom_scaffold.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class StartPage extends StatelessWidget {
|
class StartPage extends StatelessWidget {
|
||||||
@ -8,105 +9,94 @@ class StartPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SafeArea(
|
return CustomScaffold(
|
||||||
child: Scaffold(
|
children: [
|
||||||
appBar: AppBar(
|
Hero(
|
||||||
iconTheme: IconThemeData(color: CustomColors.primary),
|
tag: 'flow-icon',
|
||||||
),
|
child: Image.asset(
|
||||||
body: Padding(
|
'assets/JPEG.jpg',
|
||||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
|
height: 180,
|
||||||
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: () {
|
|
||||||
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),
|
|
||||||
))
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
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: () {
|
||||||
|
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),
|
||||||
|
))
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:app/model/services/storage_service.dart';
|
import 'package:app/model/services/storage_service.dart';
|
||||||
import 'package:app/model/view_model/base_vm.dart';
|
import 'package:app/model/view_model/base_vm.dart';
|
||||||
|
import 'package:app/pages/account_info_page.dart';
|
||||||
import 'package:app/pages/notifications_page.dart';
|
import 'package:app/pages/notifications_page.dart';
|
||||||
import 'package:app/util/colors.dart';
|
import 'package:app/util/colors.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -113,7 +114,31 @@ class _VerifyEmailPageState extends State<VerifyEmailPage> {
|
|||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: CustomColors.primary,
|
backgroundColor: CustomColors.primary,
|
||||||
),
|
),
|
||||||
onPressed: () {},
|
onPressed: () async {
|
||||||
|
final acc = await _vm.account;
|
||||||
|
if (acc != null && acc.emailVerified) {
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (builder) =>
|
||||||
|
const AccountInfoPage()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).clearSnackBars();
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(SnackBar(
|
||||||
|
backgroundColor: CustomColors.error,
|
||||||
|
content: const Text(
|
||||||
|
'E-Mail Adresse ist noch nicht verifiziert.',
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
child: const SizedBox(
|
child: const SizedBox(
|
||||||
height: 50,
|
height: 50,
|
||||||
width: 100,
|
width: 100,
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
final emailRegExp = RegExp(r"^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+");
|
final emailRegExp = RegExp(r"^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+");
|
||||||
final nameRegExp =
|
final nameRegExp = RegExp(r"^[a-zA-ZäöüÄÖÜß]+(?:[-\s][a-zA-ZäöüÄÖÜß]+)?$");
|
||||||
RegExp(r"^\s*([A-Za-z]{1,}([\.,] |[-']| ))+[A-Za-z]+\.?\s*$");
|
final phoneRegExp = RegExp(r"^(?:\+49|0)[1-9][0-9\s\-\/]*$");
|
||||||
final phoneRegExp = RegExp(r"^\+?0[0-9]{10}$");
|
final streetRegExp = RegExp(r"^[a-zA-ZäöüÄÖÜß\s\-\d]+$");
|
||||||
final passwordRegExp = RegExp(r'^[0-9a-zA-Z\-\_\.\,\*\+\=?!]{12,64}$');
|
final houseNumberRegExp = RegExp(r"^[0-9]+$");
|
||||||
|
final zipRegExp = RegExp(r"^\d{5}$");
|
||||||
|
final cityRegExp = RegExp(r"^[a-zA-ZäöüÄÖÜß\s\-\.,]+$");
|
||||||
|
final passwordRegExp = RegExp(
|
||||||
|
r'^(?=.*[a-zäöü])(?=.*[A-ZÄÖÜ])(?=.*\d)(?=.*[@$!%*?&])[A-Za-zäöüÄÖÜ\d@$!%*?&]{8,}$');
|
||||||
|
|
||||||
extension valString on String {
|
extension valString on String {
|
||||||
bool get isValidEmail {
|
bool get isValidEmail {
|
||||||
@ -13,6 +17,22 @@ extension valString on String {
|
|||||||
return nameRegExp.hasMatch(this);
|
return nameRegExp.hasMatch(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get isValidStreetAddress {
|
||||||
|
return streetRegExp.hasMatch(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isValidHouseNumber {
|
||||||
|
return houseNumberRegExp.hasMatch(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isValidZip {
|
||||||
|
return zipRegExp.hasMatch(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isValidCity {
|
||||||
|
return cityRegExp.hasMatch(this);
|
||||||
|
}
|
||||||
|
|
||||||
bool get isValidPassword {
|
bool get isValidPassword {
|
||||||
return passwordRegExp.hasMatch(this);
|
return passwordRegExp.hasMatch(this);
|
||||||
}
|
}
|
||||||
|
33
frontend/app/lib/widgets/custom_scaffold.dart
Normal file
33
frontend/app/lib/widgets/custom_scaffold.dart
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import 'package:app/util/colors.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CustomScaffold extends StatelessWidget {
|
||||||
|
const CustomScaffold({
|
||||||
|
super.key,
|
||||||
|
required this.children,
|
||||||
|
this.backButton,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Widget> children;
|
||||||
|
final Widget? backButton;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SafeArea(
|
||||||
|
child: Scaffold(
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
|
appBar: AppBar(
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
leading: backButton,
|
||||||
|
iconTheme: IconThemeData(color: CustomColors.primary),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 45, vertical: 40),
|
||||||
|
child: Column(
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -230,7 +230,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.2"
|
version: "4.0.2"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
|
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
|
||||||
|
@ -46,7 +46,6 @@ dependencies:
|
|||||||
path: ^1.8.3
|
path: ^1.8.3
|
||||||
fixnum: ^1.1.0
|
fixnum: ^1.1.0
|
||||||
provider: ^6.0.5
|
provider: ^6.0.5
|
||||||
intl: ^0.18.1
|
|
||||||
flutter_secure_storage: ^9.0.0
|
flutter_secure_storage: ^9.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
@ -75,6 +74,7 @@ flutter:
|
|||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
assets:
|
assets:
|
||||||
- assets/chat_bubbles.png
|
- assets/chat_bubbles.png
|
||||||
|
- assets/icons/icon.jpg
|
||||||
- assets/JPEG.jpg
|
- assets/JPEG.jpg
|
||||||
- lib/assets/logo_300x200.png
|
- lib/assets/logo_300x200.png
|
||||||
- lib/assets/hero-pattern-300x200.png
|
- lib/assets/hero-pattern-300x200.png
|
||||||
|
Loading…
x
Reference in New Issue
Block a user