This commit is contained in:
2026-04-06 10:55:56 +02:00
parent c6c61f1a31
commit 4930d25e58
15 changed files with 658 additions and 11 deletions

View 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",
),
);
}
});
}
}

View 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];
}

View 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,
);
}
}

View 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()));
}
});
}
}

View 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,
);
}

View 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];
}

View 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 {}

View 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);
}

View 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];
}