rework-onboarding (#7)
Onboarding completato, ora super rapido e top Reviewed-on: http://catelliub.zapto.org:3000/brontomark/flux/pulls/7 Co-authored-by: Mark M2 Macbook <marco@catelli.it> Co-committed-by: Mark M2 Macbook <marco@catelli.it>
This commit is contained in:
@@ -1,101 +0,0 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flux/core/enums/enums.dart';
|
||||
import 'package:flux/features/company/models/company_model.dart';
|
||||
import 'package:flux/features/master_data/store/data/store_repository.dart';
|
||||
import 'package:flux/features/master_data/store/models/store_model.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>();
|
||||
final StoreRepository _storeRepository = GetIt.I.get<StoreRepository>();
|
||||
StreamSubscription<AuthState>? _authSubscription;
|
||||
|
||||
SessionBloc() : super(const SessionState(status: SessionStatus.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(status: SessionStatus.unauthenticated));
|
||||
return;
|
||||
}
|
||||
// 1. Controlla se l'utente ha una Company
|
||||
final companyJson = await _supabase
|
||||
.from('company')
|
||||
.select()
|
||||
.eq('user_id', event.userId!)
|
||||
.maybeSingle();
|
||||
|
||||
if (companyJson == null) {
|
||||
emit(
|
||||
SessionState(
|
||||
status: SessionStatus.authenticatedNoCompany,
|
||||
userId: event.userId,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
CompanyModel company = CompanyModel.fromJson(companyJson);
|
||||
|
||||
// 2. Controlla i negozi
|
||||
final stores = await _storeRepository.fetchAllCompanyStores(company.id);
|
||||
|
||||
if (stores.isEmpty) {
|
||||
emit(
|
||||
SessionState(
|
||||
status: SessionStatus.authenticatedNoStore,
|
||||
userId: event.userId,
|
||||
company: company,
|
||||
),
|
||||
);
|
||||
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!);
|
||||
}
|
||||
final selectedStore = stores.firstWhere((s) => s.id == lastStoreId);
|
||||
emit(
|
||||
SessionState(
|
||||
status: SessionStatus.ready,
|
||||
userId: event.userId,
|
||||
company: company,
|
||||
selectedStore: selectedStore,
|
||||
availableStores: stores,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_authSubscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
133
lib/core/blocs/session/session_cubit.dart
Normal file
133
lib/core/blocs/session/session_cubit.dart
Normal file
@@ -0,0 +1,133 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/data/core_repository.dart';
|
||||
import 'package:flux/features/company/models/company_model.dart';
|
||||
import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
|
||||
import 'package:flux/features/master_data/store/models/store_model.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:collection/collection.dart'; // Per firstWhereOrNull
|
||||
|
||||
// Importa lo state con l'Enum e il CoreRepository...
|
||||
part 'session_state.dart';
|
||||
|
||||
class SessionCubit extends Cubit<SessionState> {
|
||||
final CoreRepository _repository;
|
||||
final SharedPreferences _prefs; // Iniettato via GetIt
|
||||
final SupabaseClient _supabase = Supabase.instance.client;
|
||||
|
||||
static const String _lastStoreKey = 'last_selected_store_id';
|
||||
|
||||
SessionCubit(this._repository, this._prefs)
|
||||
: super(const SessionState(status: SessionStatus.initial)) {
|
||||
initializeSession();
|
||||
// Possiamo metterci in ascolto dei cambiamenti di Auth (Login/Logout)
|
||||
_supabase.auth.onAuthStateChange.listen((data) {
|
||||
final AuthChangeEvent event = data.event;
|
||||
if (event == AuthChangeEvent.signedIn) {
|
||||
initializeSession();
|
||||
} else if (event == AuthChangeEvent.signedOut) {
|
||||
emit(const SessionState(status: SessionStatus.unauthenticated));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> initializeSession() async {
|
||||
final user = _supabase.auth.currentUser;
|
||||
if (user == null) {
|
||||
return emit(state.copyWith(status: SessionStatus.unauthenticated));
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Controllo Azienda
|
||||
final company = await _repository.getCompanyByOwnerId(user.id);
|
||||
if (company == null) {
|
||||
return emit(
|
||||
state.copyWith(
|
||||
status: SessionStatus.onboardingRequired,
|
||||
user: user,
|
||||
onboardingStep: OnboardingStep.company,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(state.copyWith(company: company));
|
||||
}
|
||||
|
||||
// 2. Controllo Negozi
|
||||
final stores = await _repository.getStoresByCompanyId(company.id!);
|
||||
if (stores.isEmpty) {
|
||||
return emit(
|
||||
state.copyWith(
|
||||
status: SessionStatus.onboardingRequired,
|
||||
user: user,
|
||||
company: company,
|
||||
onboardingStep: OnboardingStep.store,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(state.copyWith(currentStore: stores.first));
|
||||
}
|
||||
|
||||
// 3. Controllo Staff (Paziente Zero)
|
||||
final staff = await _repository.getStaffMemberByUserId(user.id);
|
||||
if (staff == null) {
|
||||
return emit(
|
||||
state.copyWith(
|
||||
status: SessionStatus.onboardingRequired,
|
||||
user: user,
|
||||
company: company,
|
||||
onboardingStep: OnboardingStep.staff,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- TUTTO COMPLETATO: LOGICA DEL NEGOZIO DI DEFAULT ---
|
||||
|
||||
// Leggiamo l'ultimo negozio dalle SharedPreferences
|
||||
final lastStoreId = _prefs.getString(_lastStoreKey);
|
||||
|
||||
// Cerchiamo quel negozio nella lista. Se non c'è (magari è stato eliminato), prendiamo il primo.
|
||||
final activeStore =
|
||||
stores.firstWhereOrNull((s) => s.id == lastStoreId) ?? stores.first;
|
||||
|
||||
// Se non avevamo il lastStoreId salvato, salviamolo ora
|
||||
if (lastStoreId != activeStore.id && activeStore.id != null) {
|
||||
await _prefs.setString(_lastStoreKey, activeStore.id!);
|
||||
}
|
||||
|
||||
// 4. BENVENUTO A BORDO
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: SessionStatus.authenticated,
|
||||
user: user,
|
||||
company: company,
|
||||
currentStore: activeStore,
|
||||
currentStaff: staff,
|
||||
onboardingStep: OnboardingStep.none, // Svuotiamo l'onboarding
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
// Se esplode il database, non lasciamo l'app freezata in 'initial'
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: SessionStatus
|
||||
.unauthenticated, // O un nuovo stato SessionStatus.error
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- FUNZIONE EXTRA: CAMBIO NEGOZIO DALLA DASHBOARD ---
|
||||
Future<void> changeStore(StoreModel newStore) async {
|
||||
if (newStore.id != null) {
|
||||
await _prefs.setString(_lastStoreKey, newStore.id!);
|
||||
emit(state.copyWith(currentStore: newStore));
|
||||
}
|
||||
}
|
||||
|
||||
// --- LOGOUT ---
|
||||
Future<void> signOut() async {
|
||||
await _supabase.auth.signOut();
|
||||
// Non serve emettere stato qui, ci pensa il listener nel costruttore!
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
part of 'session_bloc.dart';
|
||||
|
||||
abstract class SessionEvent {}
|
||||
|
||||
class AppStarted extends SessionEvent {}
|
||||
|
||||
class UserChanged extends SessionEvent {
|
||||
final String? userId;
|
||||
UserChanged(this.userId);
|
||||
}
|
||||
@@ -1,51 +1,69 @@
|
||||
part of 'session_bloc.dart';
|
||||
part of 'session_cubit.dart';
|
||||
|
||||
/// Definisce lo stato macroscopico della sessione
|
||||
enum SessionStatus {
|
||||
unknown,
|
||||
initial,
|
||||
unauthenticated,
|
||||
authenticatedNoCompany, // Loggato ma deve creare l'azienda
|
||||
authenticatedNoStore, // Ha l'azienda ma deve creare/scegliere il primo negozio
|
||||
ready,
|
||||
onboardingRequired,
|
||||
authenticated,
|
||||
}
|
||||
|
||||
/// Definisce lo step esatto dell'onboarding (Paranoia Mode)
|
||||
enum OnboardingStep {
|
||||
none, // Non serve onboarding
|
||||
company, // Step 1: Manca l'azienda
|
||||
store, // Step 2: Manca il negozio
|
||||
staff, // Step 3: Manca il profilo staff ("Paziente Zero")
|
||||
completed, // Flusso terminato con successo
|
||||
}
|
||||
|
||||
class SessionState extends Equatable {
|
||||
final SessionStatus status;
|
||||
final String? userId;
|
||||
final User? user; // Utente di Supabase Auth
|
||||
final CompanyModel? company;
|
||||
final StoreModel? selectedStore;
|
||||
final List<StoreModel> availableStores; // Utile per uno switcher in futuro
|
||||
final StoreModel? currentStore;
|
||||
final StaffMemberModel? currentStaff;
|
||||
final OnboardingStep onboardingStep;
|
||||
|
||||
const SessionState({
|
||||
this.status = SessionStatus.unknown,
|
||||
this.userId,
|
||||
this.status = SessionStatus.initial,
|
||||
this.user,
|
||||
this.company,
|
||||
this.selectedStore,
|
||||
this.availableStores = const [],
|
||||
this.currentStore,
|
||||
this.currentStaff,
|
||||
this.onboardingStep = OnboardingStep.none,
|
||||
});
|
||||
|
||||
/// Metodo per creare una copia dello stato modificando solo i campi necessari
|
||||
SessionState copyWith({
|
||||
SessionStatus? status,
|
||||
User? user,
|
||||
CompanyModel? company,
|
||||
StoreModel? currentStore,
|
||||
StaffMemberModel? currentStaff,
|
||||
OnboardingStep? onboardingStep,
|
||||
}) {
|
||||
return SessionState(
|
||||
status: status ?? this.status,
|
||||
user: user ?? this.user,
|
||||
company: company ?? this.company,
|
||||
currentStore: currentStore ?? this.currentStore,
|
||||
currentStaff: currentStaff ?? this.currentStaff,
|
||||
onboardingStep: onboardingStep ?? this.onboardingStep,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
status,
|
||||
userId,
|
||||
user,
|
||||
company,
|
||||
selectedStore,
|
||||
availableStores,
|
||||
currentStore,
|
||||
currentStaff,
|
||||
onboardingStep,
|
||||
];
|
||||
|
||||
// copyWith per aggiornare solo un pezzo (es. quando cambi negozio)
|
||||
SessionState copyWith({
|
||||
SessionStatus? status,
|
||||
String? userId,
|
||||
CompanyModel? company,
|
||||
StoreModel? selectedStore,
|
||||
List<StoreModel>? availableStores,
|
||||
}) {
|
||||
return SessionState(
|
||||
status: status ?? this.status,
|
||||
userId: userId ?? this.userId,
|
||||
company: company ?? this.company,
|
||||
selectedStore: selectedStore ?? this.selectedStore,
|
||||
availableStores: availableStores ?? this.availableStores,
|
||||
);
|
||||
}
|
||||
// Helper rapidi per la UI
|
||||
bool get isAuthenticated => status == SessionStatus.authenticated;
|
||||
bool get needsOnboarding => status == SessionStatus.onboardingRequired;
|
||||
}
|
||||
|
||||
111
lib/core/data/core_repository.dart
Normal file
111
lib/core/data/core_repository.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/features/company/models/company_model.dart';
|
||||
import 'package:flux/features/master_data/store/models/store_model.dart';
|
||||
import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
// Importa i tuoi modelli...
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class CoreRepository {
|
||||
final _supabase = Supabase.instance.client;
|
||||
|
||||
// --- QUERY DI SESSIONE (Uso di maybeSingle per evitare crash) ---
|
||||
|
||||
Future<CompanyModel?> getCompanyByOwnerId(String userId) async {
|
||||
try {
|
||||
final response = await _supabase
|
||||
.from('company')
|
||||
.select()
|
||||
.eq('user_id', userId) // <-- Assicurati di avere questo campo nel DB!
|
||||
.maybeSingle();
|
||||
|
||||
if (response == null) return null;
|
||||
return CompanyModel.fromMap(response);
|
||||
} catch (e) {
|
||||
debugPrint('Errore recupero azienda: $e');
|
||||
throw Exception('Errore recupero azienda: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<StoreModel>> getStoresByCompanyId(String companyId) async {
|
||||
try {
|
||||
final response = await _supabase
|
||||
.from('store')
|
||||
.select()
|
||||
.eq('company_id', companyId)
|
||||
.eq('is_active', true) // Buona pratica
|
||||
.order('nome'); // O come si chiama il campo nome
|
||||
|
||||
return (response as List).map((s) => StoreModel.fromMap(s)).toList();
|
||||
} catch (e) {
|
||||
debugPrint('Errore recupero negozi: $e');
|
||||
throw Exception('Errore recupero negozi: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<StaffMemberModel?> getStaffMemberByUserId(String userId) async {
|
||||
try {
|
||||
final response = await _supabase
|
||||
.from('staff_member')
|
||||
.select()
|
||||
.eq('user_id', userId)
|
||||
.maybeSingle();
|
||||
|
||||
if (response == null) return null;
|
||||
return StaffMemberModel.fromMap(response);
|
||||
} catch (e) {
|
||||
debugPrint('Errore recupero profilo staff: $e');
|
||||
throw Exception('Errore recupero profilo staff: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// --- MUTAZIONI PER L'ONBOARDING ---
|
||||
|
||||
Future<CompanyModel> createCompany(CompanyModel company) async {
|
||||
try {
|
||||
final response = await _supabase
|
||||
.from('company')
|
||||
.insert(company.toMap())
|
||||
.select()
|
||||
.single();
|
||||
return CompanyModel.fromMap(response);
|
||||
} catch (e) {
|
||||
debugPrint('Creazione azienda fallita: $e');
|
||||
throw Exception('Creazione azienda fallita: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<StoreModel> createStore(StoreModel store) async {
|
||||
try {
|
||||
final response = await _supabase
|
||||
.from('store')
|
||||
.insert(store.toMap())
|
||||
.select()
|
||||
.single();
|
||||
return StoreModel.fromMap(response);
|
||||
} catch (e) {
|
||||
debugPrint('Creazione negozio fallita: $e');
|
||||
throw Exception('Creazione negozio fallita: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<StaffMemberModel> createStaffMember(StaffMemberModel staff) async {
|
||||
try {
|
||||
final response = await _supabase
|
||||
.from('staff_member')
|
||||
.insert(staff.toMap())
|
||||
.select()
|
||||
.single();
|
||||
final StaffMemberModel staffMember = StaffMemberModel.fromMap(response);
|
||||
await _supabase.from('staff_in_stores').insert({
|
||||
'staff_member_id': staffMember.id,
|
||||
'store_id': GetIt.I.get<SessionCubit>().state.currentStore!.id,
|
||||
});
|
||||
return StaffMemberModel.fromMap(response);
|
||||
} catch (e) {
|
||||
debugPrint('Creazione profilo staff fallita: $e');
|
||||
throw Exception('Creazione profilo staff fallita: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,89 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
// Importa il tuo SessionCubit e lo State
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/core/data/core_repository.dart';
|
||||
import 'package:flux/features/auth/ui/auth_screen.dart';
|
||||
import 'package:flux/features/company/ui/create_company_screen.dart';
|
||||
import 'package:flux/features/customers/models/customer_model.dart';
|
||||
import 'package:flux/features/customers/ui/customer_detail_screen.dart';
|
||||
import 'package:flux/features/home/ui/home_screen.dart';
|
||||
import 'package:flux/features/master_data/products/ui/products_screen.dart';
|
||||
import 'package:flux/features/master_data/store/ui/create_store_screen.dart';
|
||||
import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart';
|
||||
import 'package:flux/features/onboarding/ui/onboarding_screen.dart';
|
||||
import 'package:flux/features/services/models/service_model.dart';
|
||||
import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'dart:async';
|
||||
|
||||
class AppRouter {
|
||||
// Funzione statica per creare il router
|
||||
static GoRouter createRouter(SessionBloc sessionBloc) {
|
||||
static GoRouter createRouter(SessionCubit sessionCubit) {
|
||||
return GoRouter(
|
||||
initialLocation: '/',
|
||||
// Ascolta i cambiamenti del Bloc per scatenare il redirect
|
||||
refreshListenable: _GoRouterRefreshStream(sessionBloc.stream),
|
||||
// MAGIA 1: Il router "ascolta" ogni singolo respiro del SessionCubit
|
||||
refreshListenable: GoRouterRefreshStream(sessionCubit.stream),
|
||||
|
||||
// MAGIA 2: Il Buttafuori Supremo
|
||||
redirect: (context, state) {
|
||||
final sessionState = sessionBloc.state;
|
||||
final sessionState = sessionCubit.state;
|
||||
final isGoingToLogin = state.matchedLocation == '/login';
|
||||
final isGoingToOnboarding = state.matchedLocation == '/onboarding';
|
||||
|
||||
// Logica di redirezione basata sugli stati del SessionBloc
|
||||
final bool isUnknown = sessionState.status == SessionStatus.unknown;
|
||||
final bool isUnauthenticated =
|
||||
sessionState.status == SessionStatus.unauthenticated;
|
||||
final bool isNoCompany =
|
||||
sessionState.status == SessionStatus.authenticatedNoCompany;
|
||||
final bool isNoStore =
|
||||
sessionState.status == SessionStatus.authenticatedNoStore;
|
||||
final bool isReady = sessionState.status == SessionStatus.ready;
|
||||
|
||||
final String location = state.matchedLocation;
|
||||
|
||||
if (isUnknown) return null; // Aspetta che l'app si svegli
|
||||
|
||||
if (isUnauthenticated && location != '/login') return '/login';
|
||||
|
||||
if (isNoCompany && location != '/create-company') {
|
||||
return '/create-company';
|
||||
// Caso 1: L'app si sta ancora avviando.
|
||||
// Restituiamo null per farlo rimanere sulla SplashScreen del main.dart
|
||||
if (sessionState.status == SessionStatus.initial) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isNoStore && location != '/create-store') return '/create-store';
|
||||
// Caso 2: Utente NON loggato.
|
||||
if (sessionState.status == SessionStatus.unauthenticated) {
|
||||
// Se sta già andando al login, lascialo andare. Altrimenti, forzalo al login.
|
||||
return isGoingToLogin ? null : '/login';
|
||||
}
|
||||
|
||||
// Se sono loggato e sto cercando di andare alla login, vai in dashboard
|
||||
if (isReady && location == '/login') return '/';
|
||||
// Caso 3: Utente loggato MA manca un pezzo dell'azienda (Flusso Canalizzatore)
|
||||
if (sessionState.status == SessionStatus.onboardingRequired) {
|
||||
// Se sta già andando all'onboarding, ok. Altrimenti forzalo lì.
|
||||
// Non può "scappare" digitando l'URL della dashboard!
|
||||
return isGoingToOnboarding ? null : '/onboarding';
|
||||
}
|
||||
|
||||
// Caso 4: Utente loggato e configurato (Tutto OK!)
|
||||
if (sessionState.status == SessionStatus.authenticated) {
|
||||
// Se per sbaglio cerca di tornare al login o all'onboarding,
|
||||
// lo rimbalziamo alla home.
|
||||
if (isGoingToLogin || isGoingToOnboarding) {
|
||||
return '/';
|
||||
}
|
||||
// Per tutte le altre rotte (dashboard, clienti, anagrafiche), lascialo passare.
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
routes: [
|
||||
GoRoute(path: '/', builder: (context, state) => const HomeScreen()),
|
||||
GoRoute(
|
||||
path: '/login',
|
||||
//builder: (context, state) => const LoginScreen(),
|
||||
builder: (context, state) => const AuthScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/create-company',
|
||||
builder: (context, state) => const CreateCompanyScreen(),
|
||||
path: '/onboarding',
|
||||
builder: (context, state) => BlocProvider(
|
||||
create: (context) => OnboardingCubit(
|
||||
GetIt.I.get<SessionCubit>(),
|
||||
GetIt.I.get<CoreRepository>(),
|
||||
),
|
||||
child: const OnboardingScreen(),
|
||||
),
|
||||
// Nota: All'interno di questa schermata useremo il PageView pilotato
|
||||
// dall'OnboardingStep. Al router non interessa quale step è attivo,
|
||||
// gli basta sapere che deve stare rinchiuso qui dentro!
|
||||
),
|
||||
GoRoute(
|
||||
path: '/create-store',
|
||||
builder: (context, state) => const CreateStoreScreen(),
|
||||
path: '/',
|
||||
builder: (context, state) => const HomeScreen(), // La tua home
|
||||
),
|
||||
GoRoute(
|
||||
path: '/customer/:id',
|
||||
@@ -96,11 +118,14 @@ class AppRouter {
|
||||
}
|
||||
}
|
||||
|
||||
// Classe di supporto per convertire lo Stream del Bloc in un Listenable per GoRouter
|
||||
class _GoRouterRefreshStream extends ChangeNotifier {
|
||||
_GoRouterRefreshStream(Stream<dynamic> stream) {
|
||||
/// Utility fondamentale per GoRouter: trasforma lo Stream del Cubit
|
||||
/// in un Listenable che GoRouter può ascoltare per forzare i redirect.
|
||||
class GoRouterRefreshStream extends ChangeNotifier {
|
||||
GoRouterRefreshStream(Stream<dynamic> stream) {
|
||||
notifyListeners();
|
||||
_subscription = stream.asBroadcastStream().listen((_) => notifyListeners());
|
||||
_subscription = stream.asBroadcastStream().listen(
|
||||
(dynamic _) => notifyListeners(),
|
||||
);
|
||||
}
|
||||
|
||||
late final StreamSubscription<dynamic> _subscription;
|
||||
|
||||
6
lib/core/utils/validators.dart
Normal file
6
lib/core/utils/validators.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
String? notEmptyValidator(String? value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Campo obbligatorio';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
// lib/ui/common/flux_text_field.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flux/core/theme/theme.dart';
|
||||
|
||||
class FluxTextField extends StatefulWidget {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final String? labelText;
|
||||
final IconData? icon;
|
||||
final bool isPassword;
|
||||
final bool autoFocus;
|
||||
final TextEditingController? controller;
|
||||
@@ -14,11 +16,16 @@ class FluxTextField extends StatefulWidget {
|
||||
final Function(String)? onSubmitted;
|
||||
final Function(String)? onChanged;
|
||||
final int? maxLength;
|
||||
final String? Function(String?)? validator;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final TextCapitalization? textCapitalization;
|
||||
final bool? autocorrect;
|
||||
|
||||
const FluxTextField({
|
||||
super.key, // Usiamo super.key per Flutter moderno
|
||||
required this.label,
|
||||
required this.icon,
|
||||
this.labelText,
|
||||
this.icon,
|
||||
this.isPassword = false,
|
||||
this.autoFocus = false,
|
||||
this.controller,
|
||||
@@ -28,6 +35,10 @@ class FluxTextField extends StatefulWidget {
|
||||
this.onSubmitted,
|
||||
this.onChanged,
|
||||
this.maxLength,
|
||||
this.validator,
|
||||
this.inputFormatters,
|
||||
this.textCapitalization,
|
||||
this.autocorrect,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -45,11 +56,13 @@ class _FluxTextFieldState extends State<FluxTextField> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
return TextFormField(
|
||||
controller: widget.controller,
|
||||
validator: widget.validator,
|
||||
obscureText: _obscureText,
|
||||
|
||||
enableSuggestions: !widget.isPassword,
|
||||
autocorrect: !widget.isPassword,
|
||||
autocorrect: widget.isPassword ? false : widget.autocorrect ?? true,
|
||||
keyboardType: widget.keyboardType,
|
||||
autofocus: widget.autoFocus,
|
||||
minLines: widget.minLines,
|
||||
@@ -57,11 +70,11 @@ class _FluxTextFieldState extends State<FluxTextField> {
|
||||
maxLines: widget.minLines != null ? null : widget.maxLines,
|
||||
style: TextStyle(color: context.primaryText),
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(
|
||||
widget.icon,
|
||||
color: context.accent.withValues(alpha: 0.6),
|
||||
),
|
||||
labelText: widget.label,
|
||||
prefixIcon: widget.icon != null
|
||||
? Icon(widget.icon, color: context.accent.withValues(alpha: 0.6))
|
||||
: null,
|
||||
|
||||
labelText: widget.labelText ?? widget.label,
|
||||
labelStyle: TextStyle(color: context.secondaryText, fontSize: 14),
|
||||
filled: true,
|
||||
fillColor: context.surface.withValues(alpha: 0.5),
|
||||
@@ -79,6 +92,7 @@ class _FluxTextFieldState extends State<FluxTextField> {
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
|
||||
suffixIcon: widget.isPassword
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
@@ -95,9 +109,12 @@ class _FluxTextFieldState extends State<FluxTextField> {
|
||||
)
|
||||
: null, // Se non è una password, niente icona
|
||||
),
|
||||
onSubmitted: widget.onSubmitted,
|
||||
onFieldSubmitted: widget.onSubmitted,
|
||||
onChanged: widget.onChanged,
|
||||
maxLength: widget.maxLength,
|
||||
inputFormatters: widget.inputFormatters,
|
||||
|
||||
textCapitalization: widget.textCapitalization ?? TextCapitalization.none,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user