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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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.dart';
|
||||||
import 'package:flux/theme/theme_bloc.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/home_screen.dart';
|
||||||
import 'package:flux/ui/settings/settings.dart';
|
import 'package:flux/ui/settings/settings.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@@ -14,9 +17,21 @@ void main() async {
|
|||||||
await SharedPreferences.getInstance(),
|
await SharedPreferences.getInstance(),
|
||||||
);
|
);
|
||||||
getIt.registerSingleton<AppSettings>(AppSettings());
|
getIt.registerSingleton<AppSettings>(AppSettings());
|
||||||
|
await Supabase.initialize(
|
||||||
|
url: 'https://pvqpjloswwvtfoxbkfbh.supabase.co',
|
||||||
|
anonKey:
|
||||||
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InB2cXBqbG9zd3d2dGZveGJrZmJoIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzQ5MjkyNjgsImV4cCI6MjA5MDUwNTI2OH0.-7nitlX1pzPGscGawlIF0vhwuD_w209FUU0PxDNGm0Y',
|
||||||
|
);
|
||||||
|
getIt.registerSingleton<SupabaseClient>(Supabase.instance.client);
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
BlocProvider(
|
MultiBlocProvider(
|
||||||
create: (context) => ThemeBloc()..add(LoadThemeEvent()),
|
providers: [
|
||||||
|
BlocProvider(create: (context) => ThemeBloc()..add(LoadThemeEvent())),
|
||||||
|
BlocProvider<SessionBloc>(
|
||||||
|
create: (context) => SessionBloc()..add(AppStarted()),
|
||||||
|
),
|
||||||
|
],
|
||||||
child: const FluxApp(),
|
child: const FluxApp(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -35,9 +50,41 @@ class FluxApp extends StatelessWidget {
|
|||||||
theme: fluxLightTheme,
|
theme: fluxLightTheme,
|
||||||
darkTheme: fluxDarkTheme,
|
darkTheme: fluxDarkTheme,
|
||||||
themeMode: state.currentTheme.themeMode, // Applica il tema FLUX
|
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 {
|
extension FluxThemeContext on BuildContext {
|
||||||
// Recupera il colore 'secondary' definito nel tuo ColorScheme (il Turchese Flux)
|
// --- Colori del Brand ---
|
||||||
Color get accent => Theme.of(this).colorScheme.secondary;
|
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:
|
// --- Superfici ---
|
||||||
Color get primary => Theme.of(this).colorScheme.primary;
|
|
||||||
Color get surface => Theme.of(this).colorScheme.surface;
|
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 =>
|
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';
|
part 'theme_state.dart';
|
||||||
|
|
||||||
class ThemeBloc extends Bloc<ThemeEvent, ThemeState> {
|
class ThemeBloc extends Bloc<ThemeEvent, ThemeState> {
|
||||||
static const String _savedThemeKey = "themeModeSetting";
|
|
||||||
final SharedPreferences _prefs = GetIt.I.get<SharedPreferences>();
|
final SharedPreferences _prefs = GetIt.I.get<SharedPreferences>();
|
||||||
ThemeBloc() : super(ThemeState(currentTheme: AppThemeMode.system)) {
|
ThemeBloc() : super(ThemeState(currentTheme: AppThemeMode.system)) {
|
||||||
on<LoadThemeEvent>((event, emit) {
|
on<LoadThemeEvent>((event, emit) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
currentTheme: AppThemeMode.fromValue(
|
currentTheme: AppThemeMode.fromValue(
|
||||||
_prefs.getString(_savedThemeKey),
|
_prefs.getString(PrefKeys.theme.value),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
on<ChangeThemeEvent>((event, emit) async {
|
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));
|
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