auth
This commit is contained in:
57
lib/blocs/auth/auth_bloc.dart
Normal file
57
lib/blocs/auth/auth_bloc.dart
Normal file
@@ -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<AuthEvent, AuthState> {
|
||||
final _supabase = GetIt.instance<SupabaseClient>();
|
||||
|
||||
AuthBloc()
|
||||
: super(const AuthState(status: AuthStatus.initial, isLoginMode: true)) {
|
||||
on<ToggleAuthMode>(
|
||||
(event, emit) => emit(state.copyWith(isLoginMode: !state.isLoginMode)),
|
||||
);
|
||||
on<LoginRequested>((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",
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
24
lib/blocs/auth/auth_events.dart
Normal file
24
lib/blocs/auth/auth_events.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
part of 'auth_bloc.dart';
|
||||
|
||||
abstract class AuthEvent extends Equatable {
|
||||
const AuthEvent();
|
||||
|
||||
@override
|
||||
List<Object?> 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<Object?> get props => [email, password, storeCode];
|
||||
}
|
||||
26
lib/blocs/auth/auth_state.dart
Normal file
26
lib/blocs/auth/auth_state.dart
Normal file
@@ -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<Object?> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
31
lib/blocs/company/company_bloc.dart
Normal file
31
lib/blocs/company/company_bloc.dart
Normal file
@@ -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<CompanyEvent, CompanyState> {
|
||||
final _supabase = GetIt.instance<SupabaseClient>();
|
||||
|
||||
CompanyBloc() : super(const CompanyState(status: CompanyStatus.initial)) {
|
||||
on<SaveCompanyRequested>((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()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
16
lib/blocs/company/company_events.dart
Normal file
16
lib/blocs/company/company_events.dart
Normal file
@@ -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,
|
||||
);
|
||||
}
|
||||
13
lib/blocs/company/company_state.dart
Normal file
13
lib/blocs/company/company_state.dart
Normal file
@@ -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<Object?> get props => [status, error];
|
||||
}
|
||||
80
lib/blocs/session/session_bloc.dart
Normal file
80
lib/blocs/session/session_bloc.dart
Normal file
@@ -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<SessionEvent, SessionState> {
|
||||
final SupabaseClient _supabase = GetIt.I.get<SupabaseClient>();
|
||||
StreamSubscription<AuthState>? _authSubscription;
|
||||
|
||||
SessionBloc() : super(const SessionState.unknown()) {
|
||||
on<AppStarted>((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<UserChanged>((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<SharedPreferences>();
|
||||
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<void> close() {
|
||||
_authSubscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
class SharedPreferencesKeys {}
|
||||
10
lib/blocs/session/session_events.dart
Normal file
10
lib/blocs/session/session_events.dart
Normal file
@@ -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);
|
||||
}
|
||||
37
lib/blocs/session/session_state.dart
Normal file
37
lib/blocs/session/session_state.dart
Normal file
@@ -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<Object?> get props => [status, userId];
|
||||
}
|
||||
@@ -18,3 +18,11 @@ enum AppThemeMode {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum PrefKeys {
|
||||
theme('themeModeSetting'),
|
||||
lastStore('lastStore');
|
||||
|
||||
const PrefKeys(this.value);
|
||||
final String value;
|
||||
}
|
||||
|
||||
@@ -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>(AppSettings());
|
||||
await Supabase.initialize(
|
||||
url: 'https://pvqpjloswwvtfoxbkfbh.supabase.co',
|
||||
anonKey:
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InB2cXBqbG9zd3d2dGZveGJrZmJoIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzQ5MjkyNjgsImV4cCI6MjA5MDUwNTI2OH0.-7nitlX1pzPGscGawlIF0vhwuD_w209FUU0PxDNGm0Y',
|
||||
);
|
||||
getIt.registerSingleton<SupabaseClient>(Supabase.instance.client);
|
||||
|
||||
runApp(
|
||||
BlocProvider(
|
||||
create: (context) => ThemeBloc()..add(LoadThemeEvent()),
|
||||
MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (context) => ThemeBloc()..add(LoadThemeEvent())),
|
||||
BlocProvider<SessionBloc>(
|
||||
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<SessionBloc, SessionState>(
|
||||
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()),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
111
lib/models/company_model.dart
Normal file
111
lib/models/company_model.dart
Normal file
@@ -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<String, dynamic> 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<String, dynamic> 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<Object?> get props => [
|
||||
id,
|
||||
userId,
|
||||
ragioneSociale,
|
||||
partitaIva,
|
||||
isPaid,
|
||||
paymentExpiration,
|
||||
];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -8,20 +8,19 @@ part 'theme_events.dart';
|
||||
part 'theme_state.dart';
|
||||
|
||||
class ThemeBloc extends Bloc<ThemeEvent, ThemeState> {
|
||||
static const String _savedThemeKey = "themeModeSetting";
|
||||
final SharedPreferences _prefs = GetIt.I.get<SharedPreferences>();
|
||||
ThemeBloc() : super(ThemeState(currentTheme: AppThemeMode.system)) {
|
||||
on<LoadThemeEvent>((event, emit) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentTheme: AppThemeMode.fromValue(
|
||||
_prefs.getString(_savedThemeKey),
|
||||
_prefs.getString(PrefKeys.theme.value),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
on<ChangeThemeEvent>((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));
|
||||
});
|
||||
}
|
||||
|
||||
176
lib/ui/auth/auth_screen.dart
Normal file
176
lib/ui/auth/auth_screen.dart
Normal file
@@ -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<AuthScreen> createState() => _AuthScreenState();
|
||||
}
|
||||
|
||||
class _AuthScreenState extends State<AuthScreen> {
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _storeController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<AuthBloc, AuthState>(
|
||||
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<AuthBloc>().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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user