From 4930d25e587e0b599e8cab3fc097bc1d05c11aaf Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Mon, 6 Apr 2026 10:55:56 +0200 Subject: [PATCH] auth --- lib/blocs/auth/auth_bloc.dart | 57 +++++++++ lib/blocs/auth/auth_events.dart | 24 ++++ lib/blocs/auth/auth_state.dart | 26 ++++ lib/blocs/company/company_bloc.dart | 31 +++++ lib/blocs/company/company_events.dart | 16 +++ lib/blocs/company/company_state.dart | 13 ++ lib/blocs/session/session_bloc.dart | 80 ++++++++++++ lib/blocs/session/session_events.dart | 10 ++ lib/blocs/session/session_state.dart | 37 ++++++ lib/data/enums.dart | 8 ++ lib/main.dart | 53 +++++++- lib/models/company_model.dart | 111 ++++++++++++++++ lib/theme/theme.dart | 22 +++- lib/theme/theme_bloc.dart | 5 +- lib/ui/auth/auth_screen.dart | 176 ++++++++++++++++++++++++++ 15 files changed, 658 insertions(+), 11 deletions(-) create mode 100644 lib/blocs/auth/auth_bloc.dart create mode 100644 lib/blocs/auth/auth_events.dart create mode 100644 lib/blocs/auth/auth_state.dart create mode 100644 lib/blocs/company/company_bloc.dart create mode 100644 lib/blocs/company/company_events.dart create mode 100644 lib/blocs/company/company_state.dart create mode 100644 lib/blocs/session/session_bloc.dart create mode 100644 lib/blocs/session/session_events.dart create mode 100644 lib/blocs/session/session_state.dart create mode 100644 lib/models/company_model.dart create mode 100644 lib/ui/auth/auth_screen.dart diff --git a/lib/blocs/auth/auth_bloc.dart b/lib/blocs/auth/auth_bloc.dart new file mode 100644 index 0000000..a04a217 --- /dev/null +++ b/lib/blocs/auth/auth_bloc.dart @@ -0,0 +1,57 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:get_it/get_it.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +part 'auth_events.dart'; +part 'auth_state.dart'; + +class AuthBloc extends Bloc { + final _supabase = GetIt.instance(); + + AuthBloc() + : super(const AuthState(status: AuthStatus.initial, isLoginMode: true)) { + on( + (event, emit) => emit(state.copyWith(isLoginMode: !state.isLoginMode)), + ); + on((event, emit) async { + emit(state.copyWith(status: AuthStatus.loading)); + try { + if (state.isLoginMode) { + // --- LOGICA LOGIN --- + await _supabase.auth.signInWithPassword( + email: event.email, + password: event.password, + ); + // Non serve emettere success qui, ci pensa il SessionBloc! + } else { + // --- LOGICA SIGNUP --- + await _supabase.auth.signUp( + email: event.email, + password: event.password, + // Qui potresti passare il "Codice Negozio" nei data dell'utente + data: {'store_code': event.storeCode}, + ); + + // Nota: Se Supabase richiede conferma email, l'utente non sarà + // loggato subito. Gestiamolo con un messaggio. + emit( + state.copyWith( + status: AuthStatus.success, + error: "Controlla la tua email per confermare l'account!", + ), + ); + } + } on AuthException catch (e) { + emit(state.copyWith(status: AuthStatus.failure, error: e.message)); + } catch (e) { + emit( + state.copyWith( + status: AuthStatus.failure, + error: "Errore imprevisto: $e", + ), + ); + } + }); + } +} diff --git a/lib/blocs/auth/auth_events.dart b/lib/blocs/auth/auth_events.dart new file mode 100644 index 0000000..31cd776 --- /dev/null +++ b/lib/blocs/auth/auth_events.dart @@ -0,0 +1,24 @@ +part of 'auth_bloc.dart'; + +abstract class AuthEvent extends Equatable { + const AuthEvent(); + + @override + List get props => []; +} + +class ToggleAuthMode extends AuthEvent {} // Passa da Login a Registrazione + +class LoginRequested extends AuthEvent { + final String email; + final String password; + final String? storeCode; + const LoginRequested({ + required this.email, + required this.password, + this.storeCode, + }); + + @override + List get props => [email, password, storeCode]; +} diff --git a/lib/blocs/auth/auth_state.dart b/lib/blocs/auth/auth_state.dart new file mode 100644 index 0000000..2fb1a87 --- /dev/null +++ b/lib/blocs/auth/auth_state.dart @@ -0,0 +1,26 @@ +part of 'auth_bloc.dart'; + +enum AuthStatus { initial, loading, success, failure } + +class AuthState extends Equatable { + const AuthState({ + required this.status, + this.error, + required this.isLoginMode, + }); + + final AuthStatus status; + final String? error; + final bool isLoginMode; + + @override + List get props => [status, error, isLoginMode]; + + AuthState copyWith({AuthStatus? status, String? error, bool? isLoginMode}) { + return AuthState( + status: status ?? this.status, + error: error, + isLoginMode: isLoginMode ?? this.isLoginMode, + ); + } +} diff --git a/lib/blocs/company/company_bloc.dart b/lib/blocs/company/company_bloc.dart new file mode 100644 index 0000000..63aa899 --- /dev/null +++ b/lib/blocs/company/company_bloc.dart @@ -0,0 +1,31 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:get_it/get_it.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +part 'company_events.dart'; +part 'company_state.dart'; + +class CompanyBloc extends Bloc { + final _supabase = GetIt.instance(); + + CompanyBloc() : super(const CompanyState(status: CompanyStatus.initial)) { + on((event, emit) async { + emit(const CompanyState(status: CompanyStatus.loading)); + try { + final userId = _supabase.auth.currentUser!.id; + + await _supabase.from('companies').insert({ + 'owner_id': userId, + 'ragione_sociale': event.ragioneSociale, + 'partita_iva': event.partitaIva, + 'codice_univoco': event.codiceUnivoco, + }); + + emit(const CompanyState(status: CompanyStatus.success)); + } catch (e) { + emit(CompanyState(status: CompanyStatus.failure, error: e.toString())); + } + }); + } +} diff --git a/lib/blocs/company/company_events.dart b/lib/blocs/company/company_events.dart new file mode 100644 index 0000000..d50cb28 --- /dev/null +++ b/lib/blocs/company/company_events.dart @@ -0,0 +1,16 @@ +part of 'company_bloc.dart'; + +abstract class CompanyEvent { + const CompanyEvent(); +} + +final class SaveCompanyRequested extends CompanyEvent { + final String ragioneSociale; + final String partitaIva; + final String codiceUnivoco; + const SaveCompanyRequested( + this.ragioneSociale, + this.partitaIva, + this.codiceUnivoco, + ); +} diff --git a/lib/blocs/company/company_state.dart b/lib/blocs/company/company_state.dart new file mode 100644 index 0000000..2bb1ecd --- /dev/null +++ b/lib/blocs/company/company_state.dart @@ -0,0 +1,13 @@ +part of 'company_bloc.dart'; + +enum CompanyStatus { initial, loading, success, failure } + +class CompanyState extends Equatable { + final CompanyStatus status; + final String? error; + + const CompanyState({required this.status, this.error}); + + @override + List get props => [status, error]; +} diff --git a/lib/blocs/session/session_bloc.dart b/lib/blocs/session/session_bloc.dart new file mode 100644 index 0000000..b986308 --- /dev/null +++ b/lib/blocs/session/session_bloc.dart @@ -0,0 +1,80 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flux/data/enums.dart'; +import 'package:get_it/get_it.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:async'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +part 'session_events.dart'; +part 'session_state.dart'; + +class SessionBloc extends Bloc { + final SupabaseClient _supabase = GetIt.I.get(); + StreamSubscription? _authSubscription; + + SessionBloc() : super(const SessionState.unknown()) { + on((event, emit) { + // 1. Controlla la sessione attuale al boot + final session = _supabase.auth.currentSession; + if (session != null) { + add(UserChanged(session.user.id)); + } else { + add(UserChanged(null)); + } + + // 2. Ascolta i cambiamenti futuri (login, logout, token scaduto) + _authSubscription = _supabase.auth.onAuthStateChange.listen((data) { + final userId = data.session?.user.id; + add(UserChanged(userId)); + }); + }); + + on((event, emit) async { + if (event.userId == null) { + emit(SessionState.unauthenticated()); + return; + } + // 1. Controlla se l'utente ha una Company + final company = await _supabase + .from('company') + .select() + .eq('user_id', event.userId!) + .maybeSingle(); + + if (company == null) { + emit(SessionState.authenticatedNoCompany(event.userId!)); + return; + } + + // 2. Controlla i negozi + final stores = await _supabase + .from('store') + .select() + .eq('company_id', company['id']); + + if (stores.isEmpty) { + emit(SessionState.authenticatedNoStore(event.userId!, company['id'])); + return; + } + + // 3. Tutto ok, gestiamo le SharedPreferences per il negozio + final prefs = GetIt.I.get(); + String? lastStoreId = prefs.getString(PrefKeys.lastStore.value); + + // Se non c'è nelle SharedPreferences, prendi il primo della lista + if (lastStoreId == null || !stores.any((s) => s['id'] == lastStoreId)) { + lastStoreId = stores.first['id']; + await prefs.setString('last_store_id', lastStoreId!); + } + }); + } + + @override + Future close() { + _authSubscription?.cancel(); + return super.close(); + } +} + +class SharedPreferencesKeys {} diff --git a/lib/blocs/session/session_events.dart b/lib/blocs/session/session_events.dart new file mode 100644 index 0000000..8f6accb --- /dev/null +++ b/lib/blocs/session/session_events.dart @@ -0,0 +1,10 @@ +part of 'session_bloc.dart'; + +abstract class SessionEvent {} + +class AppStarted extends SessionEvent {} + +class UserChanged extends SessionEvent { + final String? userId; + UserChanged(this.userId); +} diff --git a/lib/blocs/session/session_state.dart b/lib/blocs/session/session_state.dart new file mode 100644 index 0000000..300765f --- /dev/null +++ b/lib/blocs/session/session_state.dart @@ -0,0 +1,37 @@ +part of 'session_bloc.dart'; + +enum SessionStatus { + unknown, + unauthenticated, + authenticatedNoCompany, // Loggato ma deve creare l'azienda + authenticatedNoStore, // Ha l'azienda ma deve creare/scegliere il primo negozio + ready, +} + +class SessionState extends Equatable { + final SessionStatus status; + final String? userId; + final String? companyId; + + const SessionState._({ + this.status = SessionStatus.unknown, + this.userId, + this.companyId, + }); + const SessionState.unknown() : this._(); + const SessionState.unauthenticated() + : this._(status: SessionStatus.unauthenticated); + const SessionState.authenticatedNoCompany(String userId) + : this._(status: SessionStatus.authenticatedNoCompany, userId: userId); + const SessionState.authenticatedNoStore(String userId, String companyId) + : this._( + status: SessionStatus.authenticatedNoStore, + userId: userId, + companyId: companyId, + ); + const SessionState.ready(String userId) + : this._(status: SessionStatus.ready, userId: userId); + + @override + List get props => [status, userId]; +} diff --git a/lib/data/enums.dart b/lib/data/enums.dart index 27f27a5..72b5998 100644 --- a/lib/data/enums.dart +++ b/lib/data/enums.dart @@ -18,3 +18,11 @@ enum AppThemeMode { ); } } + +enum PrefKeys { + theme('themeModeSetting'), + lastStore('lastStore'); + + const PrefKeys(this.value); + final String value; +} diff --git a/lib/main.dart b/lib/main.dart index 6fa53f8..3d93edf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/blocs/session/session_bloc.dart'; import 'package:flux/theme/theme.dart'; import 'package:flux/theme/theme_bloc.dart'; +import 'package:flux/ui/auth/auth_screen.dart'; import 'package:flux/ui/home_screen.dart'; import 'package:flux/ui/settings/settings.dart'; import 'package:get_it/get_it.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -14,9 +17,21 @@ void main() async { await SharedPreferences.getInstance(), ); getIt.registerSingleton(AppSettings()); + await Supabase.initialize( + url: 'https://pvqpjloswwvtfoxbkfbh.supabase.co', + anonKey: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InB2cXBqbG9zd3d2dGZveGJrZmJoIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzQ5MjkyNjgsImV4cCI6MjA5MDUwNTI2OH0.-7nitlX1pzPGscGawlIF0vhwuD_w209FUU0PxDNGm0Y', + ); + getIt.registerSingleton(Supabase.instance.client); + runApp( - BlocProvider( - create: (context) => ThemeBloc()..add(LoadThemeEvent()), + MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => ThemeBloc()..add(LoadThemeEvent())), + BlocProvider( + create: (context) => SessionBloc()..add(AppStarted()), + ), + ], child: const FluxApp(), ), ); @@ -35,9 +50,41 @@ class FluxApp extends StatelessWidget { theme: fluxLightTheme, darkTheme: fluxDarkTheme, themeMode: state.currentTheme.themeMode, // Applica il tema FLUX - home: const HomeScreen(), + home: const AuthGuard(), ); }, ); } } + +class AuthGuard extends StatelessWidget { + const AuthGuard({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + switch (state.status) { + case SessionStatus.unauthenticated: + return const AuthScreen(); + + case SessionStatus.authenticatedNoCompany: + // Pagina forzata per inserimento P.IVA e Ragione Sociale + return const CreateCompanyScreen(); + + case SessionStatus.authenticatedNoStore: + // Pagina forzata per creare il primo punto vendita + return const CreateStoreScreen(); + + case SessionStatus.ready: + return const HomeScreen(); // Entra direttamente nel negozio salvato + + default: + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + }, + ); + } +} diff --git a/lib/models/company_model.dart b/lib/models/company_model.dart new file mode 100644 index 0000000..204f3ef --- /dev/null +++ b/lib/models/company_model.dart @@ -0,0 +1,111 @@ +import 'package:equatable/equatable.dart'; + +class CompanyModel extends Equatable { + final String id; + final DateTime createdAt; + final String userId; + final String ragioneSociale; + final String indirizzo; + final String cap; + final String citta; + final String provincia; + final String partitaIva; + final String codiceFiscale; + final String codiceUnivoco; + final bool isPaid; + final DateTime? paymentExpiration; + + const CompanyModel({ + required this.id, + required this.createdAt, + required this.userId, + required this.ragioneSociale, + required this.indirizzo, + required this.cap, + required this.citta, + required this.provincia, + required this.partitaIva, + required this.codiceFiscale, + required this.codiceUnivoco, + required this.isPaid, + this.paymentExpiration, + }); + + // --- FROM JSON (Dall'input di Supabase a Dart) --- + factory CompanyModel.fromJson(Map json) { + return CompanyModel( + id: json['id'], + createdAt: DateTime.parse(json['created_at']), + userId: json['user_id'], + ragioneSociale: json['ragione_sociale'], + indirizzo: json['indirizzo'], + cap: json['cap'], + citta: json['citta'], + provincia: json['provincia'], + partitaIva: json['partita_iva'], + codiceFiscale: json['codice_fiscale'], + codiceUnivoco: json['codice_univoco'], + isPaid: json['is_paid'] ?? false, + paymentExpiration: json['payment_expiration'] != null + ? DateTime.parse(json['payment_expiration']) + : null, + ); + } + + // --- TO JSON (Da Dart a Supabase) --- + Map toJson() { + return { + 'ragione_sociale': ragioneSociale, + 'indirizzo': indirizzo, + 'cap': cap, + 'citta': citta, + 'provincia': provincia, + 'partita_iva': partitaIva, + 'codice_fiscale': codiceFiscale, + 'codice_univoco': codiceUnivoco, + 'is_paid': isPaid, + 'payment_expiration': paymentExpiration?.toIso8601String(), + // 'id', 'created_at' e 'user_id' di solito sono gestiti dal DB in fase di insert + }; + } + + // --- COPY WITH (Per aggiornamenti parziali) --- + CompanyModel copyWith({ + String? ragioneSociale, + String? indirizzo, + String? cap, + String? citta, + String? provincia, + String? partitaIva, + String? codiceFiscale, + String? codiceUnivoco, + bool? isPaid, + DateTime? paymentExpiration, + }) { + return CompanyModel( + id: id, + createdAt: createdAt, + userId: userId, + ragioneSociale: ragioneSociale ?? this.ragioneSociale, + indirizzo: indirizzo ?? this.indirizzo, + cap: cap ?? this.cap, + citta: citta ?? this.citta, + provincia: provincia ?? this.provincia, + partitaIva: partitaIva ?? this.partitaIva, + codiceFiscale: codiceFiscale ?? this.codiceFiscale, + codiceUnivoco: codiceUnivoco ?? this.codiceUnivoco, + isPaid: isPaid ?? this.isPaid, + paymentExpiration: paymentExpiration ?? this.paymentExpiration, + ); + } + + @override + List get props => [ + id, + userId, + ragioneSociale, + partitaIva, + isPaid, + paymentExpiration, + ]; +} diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index 317f36d..10a5439 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -243,12 +243,24 @@ ThemeData fluxLightTheme = ThemeData( ); extension FluxThemeContext on BuildContext { - // Recupera il colore 'secondary' definito nel tuo ColorScheme (il Turchese Flux) - Color get accent => Theme.of(this).colorScheme.secondary; + // --- Colori del Brand --- + Color get primary => Theme.of(this).colorScheme.primary; // Blu Flux + Color get accent => Theme.of(this).colorScheme.secondary; // Turchese Flux - // Puoi aggiungere anche questi per comodità futura: - Color get primary => Theme.of(this).colorScheme.primary; + // --- Superfici --- Color get surface => Theme.of(this).colorScheme.surface; + Color get background => + Theme.of(this).colorScheme.surfaceContainerHighest; // O background + + // --- Testi (La parte mancante) --- + // Mappiamo primaryText sul colore del titolo e secondaryText su quello del corpo Color get primaryText => - Theme.of(this).textTheme.titleLarge?.color ?? Colors.black; + Theme.of(this).textTheme.titleLarge?.color ?? Colors.white; + Color get secondaryText => + Theme.of(this).textTheme.bodyMedium?.color ?? Colors.grey; + + // Opzionale: un colore ancora più tenue per suggerimenti o icone disabilitate + Color get hintText => + Theme.of(this).textTheme.bodySmall?.color ?? + Colors.grey.withValues(alpha: 0.5); } diff --git a/lib/theme/theme_bloc.dart b/lib/theme/theme_bloc.dart index 4863b92..77b6664 100644 --- a/lib/theme/theme_bloc.dart +++ b/lib/theme/theme_bloc.dart @@ -8,20 +8,19 @@ part 'theme_events.dart'; part 'theme_state.dart'; class ThemeBloc extends Bloc { - static const String _savedThemeKey = "themeModeSetting"; final SharedPreferences _prefs = GetIt.I.get(); ThemeBloc() : super(ThemeState(currentTheme: AppThemeMode.system)) { on((event, emit) { emit( state.copyWith( currentTheme: AppThemeMode.fromValue( - _prefs.getString(_savedThemeKey), + _prefs.getString(PrefKeys.theme.value), ), ), ); }); on((event, emit) async { - await _prefs.setString(_savedThemeKey, event.appThemeMode.value); + await _prefs.setString(PrefKeys.theme.value, event.appThemeMode.value); emit(state.copyWith(currentTheme: event.appThemeMode)); }); } diff --git a/lib/ui/auth/auth_screen.dart b/lib/ui/auth/auth_screen.dart new file mode 100644 index 0000000..d34a837 --- /dev/null +++ b/lib/ui/auth/auth_screen.dart @@ -0,0 +1,176 @@ +// lib/ui/auth/auth_screen.dart +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/blocs/auth/auth_bloc.dart'; +import 'package:flux/theme/theme.dart'; + +class AuthScreen extends StatefulWidget { + const AuthScreen({super.key}); + + @override + State createState() => _AuthScreenState(); +} + +class _AuthScreenState extends State { + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + final _storeController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (state.status == AuthStatus.failure) { + // Mostra l'errore che arriva da Supabase (es. "Invalid login credentials") + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.error!), backgroundColor: Colors.red), + ); + } + }, + builder: (context, state) { + return Column( + children: [ + _AuthTextField( + label: 'Email', + icon: Icons.email, + controller: _emailController, + ), + _AuthTextField( + label: 'Password', + icon: Icons.lock, + isPassword: true, + controller: _passwordController, + ), + if (!state.isLoginMode) + _AuthTextField( + label: 'Codice Negozio', + icon: Icons.store, + controller: _storeController, + ), + + ElevatedButton( + onPressed: state.status == AuthStatus.loading + ? null + : () { + context.read().add( + LoginRequested( + email: _emailController.text.trim(), + password: _passwordController.text.trim(), + storeCode: _storeController.text.trim(), + ), + ); + }, + child: state.status == AuthStatus.loading + ? const CircularProgressIndicator() + : Text(state.isLoginMode ? 'ACCEDI' : 'REGISTRATI'), + ), + ], + ); + }, + ); + } + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + _storeController.dispose(); + super.dispose(); + } +} + +class _FluxLogo extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Column( + children: [ + Icon( + Icons.all_inclusive, + size: 80, + color: context.accent, + ), // Simbolo Flux/Infinito + Text( + 'FLUX', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.w900, + letterSpacing: 8, + ), + ), + ], + ); + } +} + +class _AuthTextField extends StatefulWidget { + final String label; + final IconData icon; + final bool isPassword; + final TextEditingController? controller; // Aggiunto per recuperare i dati + + const _AuthTextField({ + required this.label, + required this.icon, + this.isPassword = false, + this.controller, + }); + + @override + State<_AuthTextField> createState() => _AuthTextFieldState(); +} + +class _AuthTextFieldState extends State<_AuthTextField> { + bool _obscureText = true; // Stato interno per la visibilità + + @override + Widget build(BuildContext context) { + return TextField( + controller: widget.controller, + obscureText: widget.isPassword ? _obscureText : false, + style: TextStyle(color: context.primaryText), + decoration: InputDecoration( + prefixIcon: Icon( + widget.icon, + color: context.accent.withValues(alpha: 0.6), + ), + labelText: widget.label, + labelStyle: TextStyle(color: context.secondaryText, fontSize: 14), + filled: true, + fillColor: context.surface.withValues(alpha: 0.5), + + // --- LOGICA OCCHIO PASSWORD --- + suffixIcon: widget.isPassword + ? IconButton( + icon: Icon( + _obscureText + ? Icons.visibility_off_outlined + : Icons.visibility_outlined, + color: context.secondaryText, + size: 20, + ), + onPressed: () { + setState(() { + _obscureText = !_obscureText; + }); + }, + ) + : null, + + // --- BORDI STILE FLUX --- + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: context.secondaryText.withValues(alpha: 0.1), + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: context.accent, width: 1.5), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + ), + ); + } +}