Implemented settings, new scan, enhanced UI/UX
This commit is contained in:
@ -1,25 +1,48 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
|
||||
import 'package:hl_lieferservice/feature/authentication/bloc/auth_state.dart';
|
||||
import 'package:hl_lieferservice/repository/user_repository.dart';
|
||||
import 'package:hl_lieferservice/feature/authentication/service/userinfo.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hl_lieferservice/main.dart';
|
||||
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
|
||||
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
|
||||
|
||||
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
UserRepository repository;
|
||||
UserInfoService service;
|
||||
OperationBloc operationBloc;
|
||||
|
||||
AuthBloc({required this.repository, required this.operationBloc})
|
||||
: super(Unauthenticated()) {
|
||||
on<Authenticate>(_auth);
|
||||
AuthBloc({required this.service, required this.operationBloc})
|
||||
: super(Unauthenticated()) {
|
||||
on<SetAuthenticatedEvent>(_auth);
|
||||
on<Logout>(_logout);
|
||||
}
|
||||
|
||||
Future<void> _auth(Authenticate event, Emitter<AuthState> emit) async {
|
||||
Future<void> _auth(
|
||||
SetAuthenticatedEvent event,
|
||||
Emitter<AuthState> emit,
|
||||
) async {
|
||||
operationBloc.add(LoadOperation());
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
emit(Authenticated(teamId: event.username));
|
||||
operationBloc.add(FinishOperation());
|
||||
|
||||
try {
|
||||
debugPrint("Retrieve user information");
|
||||
|
||||
var response = await service.getUserinfo(event.sessionId);
|
||||
var state = Authenticated(sessionId: event.sessionId, user: response);
|
||||
locator.registerSingleton<Authenticated>(state);
|
||||
emit(state);
|
||||
operationBloc.add(FinishOperation());
|
||||
} catch (err, st) {
|
||||
debugPrint("Failed to retrieve user information");
|
||||
debugPrint(err.toString());
|
||||
debugPrint(st.toString());
|
||||
|
||||
operationBloc.add(
|
||||
FailOperation(
|
||||
message: "Login war nicht erfolgreich. Probieren Sie es erneut.",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _logout(Logout event, Emitter<AuthState> emit) async {
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
abstract class AuthEvent {}
|
||||
|
||||
class Authenticate extends AuthEvent {
|
||||
String username;
|
||||
String password;
|
||||
class SetAuthenticatedEvent extends AuthEvent {
|
||||
String sessionId;
|
||||
|
||||
Authenticate({required this.username, required this.password});
|
||||
SetAuthenticatedEvent({required this.sessionId});
|
||||
}
|
||||
|
||||
class Logout extends AuthEvent {
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import 'package:hl_lieferservice/feature/authentication/model/user.dart';
|
||||
|
||||
abstract class AuthState {}
|
||||
|
||||
class Unauthenticated extends AuthState {}
|
||||
class Authenticated extends AuthState {
|
||||
String teamId;
|
||||
|
||||
Authenticated({required this.teamId});
|
||||
}
|
||||
User user;
|
||||
String sessionId;
|
||||
|
||||
Authenticated({required this.user, required this.sessionId});
|
||||
}
|
||||
1
lib/feature/authentication/exceptions.dart
Normal file
1
lib/feature/authentication/exceptions.dart
Normal file
@ -0,0 +1 @@
|
||||
class UserUnauthorized implements Exception {}
|
||||
18
lib/feature/authentication/model/user.dart
Normal file
18
lib/feature/authentication/model/user.dart
Normal file
@ -0,0 +1,18 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'user.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class User {
|
||||
User({ required this.number, required this.firstName, required this.lastName, required this.mail });
|
||||
|
||||
String number;
|
||||
String lastName;
|
||||
String firstName;
|
||||
String mail;
|
||||
|
||||
factory User.fromJson(Map<String, dynamic> json) =>
|
||||
_$UserFromJson(json);
|
||||
|
||||
Map<dynamic, dynamic> toJson() => _$UserToJson(this);
|
||||
}
|
||||
21
lib/feature/authentication/model/user.g.dart
Normal file
21
lib/feature/authentication/model/user.g.dart
Normal file
@ -0,0 +1,21 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'user.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
User _$UserFromJson(Map<String, dynamic> json) => User(
|
||||
number: json['number'] as String,
|
||||
firstName: json['firstName'] as String,
|
||||
lastName: json['lastName'] as String,
|
||||
mail: json['mail'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
|
||||
'number': instance.number,
|
||||
'lastName': instance.lastName,
|
||||
'firstName': instance.firstName,
|
||||
'mail': instance.mail,
|
||||
};
|
||||
@ -1,11 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
|
||||
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
|
||||
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
|
||||
import 'package:hl_lieferservice/widget/operations/presentation/operation_view_enforcer.dart';
|
||||
|
||||
import '../bloc/auth_bloc.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'dart:async';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
@ -16,27 +15,111 @@ class LoginPage extends StatefulWidget {
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final _loginFormKey = GlobalKey<FormState>();
|
||||
final TextEditingController _passwordEditingController =
|
||||
TextEditingController();
|
||||
final TextEditingController _userIdEditingController =
|
||||
TextEditingController();
|
||||
bool _isLoading = false;
|
||||
late AppLinks _appLinks;
|
||||
StreamSubscription<Uri>? _linkSubscription;
|
||||
|
||||
bool _isEmpty = false;
|
||||
|
||||
void onChanged(String value) {
|
||||
setState(() {
|
||||
_isEmpty = value.isEmpty;
|
||||
});
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_appLinks = AppLinks();
|
||||
}
|
||||
|
||||
void _onPressLogin(BuildContext context) async {
|
||||
if (context.mounted) {
|
||||
context.read<AuthBloc>().add(
|
||||
Authenticate(
|
||||
username: _userIdEditingController.text,
|
||||
password: _passwordEditingController.text,
|
||||
),
|
||||
@override
|
||||
void dispose() {
|
||||
_linkSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onPressLogin() async {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
debugPrint("🔵 Setting up deep link listener...");
|
||||
|
||||
final completer = Completer<Uri>();
|
||||
|
||||
// Listen for deep links BEFORE opening browser
|
||||
_linkSubscription = _appLinks.uriLinkStream.listen(
|
||||
(Uri uri) {
|
||||
debugPrint("🟢 Deep link received: $uri");
|
||||
if (uri.scheme == 'myapp' && !completer.isCompleted) {
|
||||
completer.complete(uri);
|
||||
}
|
||||
},
|
||||
onError: (err) {
|
||||
debugPrint("🔴 Deep link error: $err");
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Small delay to ensure listener is ready
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
debugPrint("🔵 Opening browser to: http://localhost:3000/login");
|
||||
|
||||
final loginUrl = Uri.parse('http://192.168.1.9:3000/login');
|
||||
final launched = await launchUrl(
|
||||
loginUrl,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
|
||||
if (!launched) {
|
||||
throw Exception('Could not launch browser');
|
||||
}
|
||||
|
||||
debugPrint("🔵 Browser opened. Waiting for callback...");
|
||||
|
||||
// Wait for the deep link callback
|
||||
final callbackUri = await completer.future.timeout(
|
||||
const Duration(minutes: 5),
|
||||
onTimeout: () {
|
||||
debugPrint("⏱️ Timeout - no callback received");
|
||||
throw TimeoutException('Login timeout');
|
||||
},
|
||||
);
|
||||
|
||||
final sessionId = callbackUri.queryParameters['session_id']!;
|
||||
|
||||
debugPrint("✅ Success! Callback: $callbackUri");
|
||||
debugPrint("✅ Session ID: $sessionId");
|
||||
|
||||
await _linkSubscription?.cancel();
|
||||
_linkSubscription = null;
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Login erfolgreich!'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
|
||||
context.read<AuthBloc>().add(SetAuthenticatedEvent(sessionId: sessionId));
|
||||
}
|
||||
|
||||
} on TimeoutException {
|
||||
debugPrint("❌ Timeout");
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Login Timeout')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("❌ Error: $e");
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Fehler: $e')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await _linkSubscription?.cancel();
|
||||
_linkSubscription = null;
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,38 +158,19 @@ class _LoginPageState extends State<LoginPage> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Personalnummer",
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(10.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
controller: _userIdEditingController,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Passwort",
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
),
|
||||
),
|
||||
controller: _passwordEditingController,
|
||||
obscureText: true,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 15, bottom: 15),
|
||||
child: OutlinedButton(
|
||||
onPressed:
|
||||
!_isEmpty ? () => _onPressLogin(context) : null,
|
||||
child: const Text("Anmelden"),
|
||||
child: _isLoading
|
||||
? const Column(
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Warte auf Login...'),
|
||||
],
|
||||
)
|
||||
: OutlinedButton(
|
||||
onPressed: _onPressLogin,
|
||||
child: const Text("Anmelden mit Holzleitner Login"),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -118,4 +182,4 @@ class _LoginPageState extends State<LoginPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
lib/feature/authentication/service/userinfo.dart
Normal file
21
lib/feature/authentication/service/userinfo.dart
Normal file
@ -0,0 +1,21 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:hl_lieferservice/feature/authentication/model/user.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
class UserInfoService {
|
||||
String url;
|
||||
|
||||
UserInfoService({ required this.url });
|
||||
|
||||
Future<User> getUserinfo(String sessionId) async {
|
||||
var headers = {
|
||||
"Cookie": "session_id=$sessionId"
|
||||
};
|
||||
|
||||
var result = await get(Uri.parse("$url/userinfo"), headers: headers);
|
||||
debugPrint("USERINFO: ${result.body}");
|
||||
return User.fromJson(jsonDecode(result.body));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user