diff --git a/lib/components/login/login.dart b/lib/components/login/login.dart index 6ed9741..af8a5aa 100644 --- a/lib/components/login/login.dart +++ b/lib/components/login/login.dart @@ -1,10 +1,37 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:mileograph_flutter/services/authservice.dart'; import 'package:provider/provider.dart'; -class LoginScreen extends StatelessWidget { +class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + bool _checkingSession = true; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => _checkExistingSession()); + } + + Future _checkExistingSession() async { + final auth = context.read(); + try { + final valid = await auth.validateStoredToken(); + if (!valid) return; + await auth.tryRestoreSession(); + if (!mounted) return; + context.go('/'); + } finally { + if (mounted) setState(() => _checkingSession = false); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -24,7 +51,7 @@ class LoginScreen extends StatelessWidget { color: Theme.of(context).textTheme.bodyLarge?.color, ), ), - TextSpan( + const TextSpan( text: "O", style: TextStyle(color: Colors.red), ), @@ -35,18 +62,27 @@ class LoginScreen extends StatelessWidget { ), ), ], - style: TextStyle( + style: const TextStyle( decoration: TextDecoration.none, color: Colors.white, - fontFamily: "Tomatoes", - fontSize: 50, + fontFamily: "Tomatoes", + fontSize: 50, + ), ), ), - ), - const SizedBox(height: 50), - LoginPanel(), - ], - ), + if (_checkingSession) + const Padding( + padding: EdgeInsets.only(top: 12), + child: SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + const SizedBox(height: 50), + const LoginPanel(), + ], + ), ), ), ); diff --git a/lib/components/pages/new_entry.dart b/lib/components/pages/new_entry.dart index b8e6cb5..ed4bfb9 100644 --- a/lib/components/pages/new_entry.dart +++ b/lib/components/pages/new_entry.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'package:mileograph_flutter/components/calculator/calculator.dart'; import 'package:mileograph_flutter/components/pages/traction.dart'; @@ -444,10 +445,12 @@ class _NewEntryPageState extends State { }); _startController.text = data['start'] ?? ''; _endController.text = data['end'] ?? ''; - _headcodeController.text = data['headcode'] ?? ''; + _headcodeController.text = + data['headcode'] is String ? data['headcode'].toUpperCase() : ''; _notesController.text = data['notes'] ?? ''; _mileageController.text = data['mileage'] ?? ''; - _networkController.text = data['network'] ?? ''; + _networkController.text = + data['network'] is String ? data['network'].toUpperCase() : ''; } catch (_) { // Ignore corrupt draft data } finally { @@ -572,9 +575,11 @@ class _NewEntryPageState extends State { ), ), ], - ), + ), TextFormField( controller: _headcodeController, + textCapitalization: TextCapitalization.characters, + inputFormatters: const [_UpperCaseTextFormatter()], decoration: const InputDecoration( labelText: 'Headcode', border: OutlineInputBorder(), @@ -582,6 +587,8 @@ class _NewEntryPageState extends State { ), TextFormField( controller: _networkController, + textCapitalization: TextCapitalization.characters, + inputFormatters: const [_UpperCaseTextFormatter()], decoration: const InputDecoration( labelText: 'Network', border: OutlineInputBorder(), @@ -817,6 +824,21 @@ class _NewEntryPageState extends State { } } +class _UpperCaseTextFormatter extends TextInputFormatter { + const _UpperCaseTextFormatter(); + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + return newValue.copyWith( + text: newValue.text.toUpperCase(), + selection: newValue.selection, + ); + } +} + class _CalculatorPickerPage extends StatelessWidget { const _CalculatorPickerPage({required this.onResult}); final ValueChanged onResult; diff --git a/lib/services/authservice.dart b/lib/services/authservice.dart index ecedfa2..1b5f572 100644 --- a/lib/services/authservice.dart +++ b/lib/services/authservice.dart @@ -72,8 +72,8 @@ class AuthService extends ChangeNotifier { Future tryRestoreSession() async { if (_restoring || _user != null) return; - _restoring = true; - try { + _restoring = true; + try { // read token from secure storage (with fallback) final token = await _tokenStorage.getToken(); if (token == null || token.isEmpty) return; @@ -100,6 +100,24 @@ class AuthService extends ChangeNotifier { } } + Future validateStoredToken() async { + final token = await _tokenStorage.getToken(); + if (token == null || token.isEmpty) return false; + try { + await api.get( + '/validate', + headers: { + 'Authorization': 'Bearer $token', + 'accept': 'application/json', + }, + ); + return true; + } catch (_) { + await _clearToken(); + return false; + } + } + Future _persistToken(String token) async { await _tokenStorage.setToken(token); } diff --git a/pubspec.yaml b/pubspec.yaml index 98f00ae..bf56c88 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.1.2+1 +version: 0.1.3+1 environment: sdk: ^3.8.1