2026-05-31 19:04:48 +02:00
|
|
|
import 'dart:async';
|
2026-05-30 12:12:14 +02:00
|
|
|
import 'dart:io';
|
|
|
|
|
|
2026-04-22 11:06:02 +02:00
|
|
|
import 'package:equatable/equatable.dart';
|
2026-05-30 12:12:14 +02:00
|
|
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
|
|
|
import 'package:flutter/foundation.dart';
|
2026-04-22 11:06:02 +02:00
|
|
|
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';
|
2026-05-30 12:12:14 +02:00
|
|
|
import 'package:get_it/get_it.dart';
|
2026-04-22 11:06:02 +02:00
|
|
|
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 {
|
2026-05-31 19:04:48 +02:00
|
|
|
// Riportiamo lo stato su initial per far girare lo spinner se stiamo riprovando
|
|
|
|
|
emit(state.copyWith(status: SessionStatus.initial, errorMessage: null));
|
2026-04-28 15:33:38 +02:00
|
|
|
|
2026-05-31 19:04:48 +02:00
|
|
|
// WRAP DELLA LOGICA IN UN BLOCCO PROTETTO DA TIMEOUT (10 Secondi)
|
|
|
|
|
await Future(() async {
|
|
|
|
|
StaffMemberModel? staff = await _repository.getStaffMemberByUserId(
|
|
|
|
|
user.id,
|
2026-04-22 11:06:02 +02:00
|
|
|
);
|
2026-05-31 19:04:48 +02:00
|
|
|
CompanyModel? company;
|
|
|
|
|
|
|
|
|
|
if (staff != null) {
|
|
|
|
|
if (staff.hasJoined == false) {
|
|
|
|
|
await _repository.updateStaffMember(staff.id!, {
|
|
|
|
|
'has_joined': true,
|
|
|
|
|
});
|
|
|
|
|
staff = staff.copyWith(hasJoined: true);
|
|
|
|
|
}
|
|
|
|
|
company = await _repository.getCompanyById(staff.companyId);
|
|
|
|
|
} else {
|
|
|
|
|
company = await _repository.getCompanyByOwnerId(user.id);
|
|
|
|
|
}
|
2026-04-22 11:06:02 +02:00
|
|
|
|
2026-05-31 19:04:48 +02:00
|
|
|
if (company == null) {
|
|
|
|
|
return emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
status: SessionStatus.onboardingRequired,
|
|
|
|
|
user: user,
|
|
|
|
|
onboardingStep: OnboardingStep.company,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
emit(state.copyWith(company: company));
|
|
|
|
|
}
|
2026-04-22 11:06:02 +02:00
|
|
|
|
2026-05-31 19:04:48 +02:00
|
|
|
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));
|
|
|
|
|
}
|
2026-04-22 11:06:02 +02:00
|
|
|
|
2026-05-31 19:04:48 +02:00
|
|
|
if (staff == null) {
|
|
|
|
|
return emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
status: SessionStatus.onboardingRequired,
|
|
|
|
|
user: user,
|
|
|
|
|
company: company,
|
|
|
|
|
onboardingStep: OnboardingStep.staff,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-22 11:06:02 +02:00
|
|
|
|
2026-05-31 19:04:48 +02:00
|
|
|
final lastStoreId = _prefs.getString(_lastStoreKey);
|
|
|
|
|
final activeStore =
|
|
|
|
|
stores.firstWhereOrNull((s) => s.id == lastStoreId) ?? stores.first;
|
2026-04-22 11:06:02 +02:00
|
|
|
|
2026-05-31 19:04:48 +02:00
|
|
|
if (lastStoreId != activeStore.id && activeStore.id != null) {
|
|
|
|
|
await _prefs.setString(_lastStoreKey, activeStore.id!);
|
|
|
|
|
}
|
2026-04-22 11:06:02 +02:00
|
|
|
|
2026-05-31 19:04:48 +02:00
|
|
|
setIsSingleUserMode(_prefs.getBool('isSingleUserMode') ?? false);
|
2026-04-22 11:06:02 +02:00
|
|
|
|
2026-05-31 19:04:48 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
status: SessionStatus.authenticated,
|
|
|
|
|
user: user,
|
|
|
|
|
company: company,
|
|
|
|
|
currentStore: activeStore,
|
|
|
|
|
currentStaffMember: staff,
|
|
|
|
|
onboardingStep: OnboardingStep.none,
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-05-13 15:55:06 +02:00
|
|
|
|
2026-05-31 19:04:48 +02:00
|
|
|
// FCM è fuori dall'await principale, quindi va bene così
|
|
|
|
|
_registerFcmToken(companyId: company.id!, staffId: staff.id!);
|
|
|
|
|
}).timeout(
|
|
|
|
|
const Duration(seconds: 10), // Tempo massimo concesso al server
|
|
|
|
|
onTimeout: () {
|
|
|
|
|
throw TimeoutException(
|
|
|
|
|
'Il server di FLUX non risponde. Controlla la connessione.',
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
} on TimeoutException catch (e) {
|
|
|
|
|
// 🎯 BINGO! IL TIMEOUT È SCATTATO
|
|
|
|
|
debugPrint("Timeout Inizializzazione: ${e.message}");
|
2026-04-22 11:06:02 +02:00
|
|
|
emit(
|
2026-05-31 19:04:48 +02:00
|
|
|
state.copyWith(status: SessionStatus.error, errorMessage: e.message),
|
2026-04-22 11:06:02 +02:00
|
|
|
);
|
|
|
|
|
} catch (e) {
|
2026-05-31 19:04:48 +02:00
|
|
|
// Altri errori generici del DB o di rete
|
|
|
|
|
debugPrint("Errore Inizializzazione: $e");
|
2026-04-22 11:06:02 +02:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
2026-05-31 19:04:48 +02:00
|
|
|
status: SessionStatus.error,
|
|
|
|
|
errorMessage: "Si è verificato un errore di connessione imprevisto.",
|
2026-04-22 11:06:02 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 12:12:14 +02:00
|
|
|
Future<void> _registerFcmToken({
|
|
|
|
|
required String companyId,
|
|
|
|
|
required String staffId,
|
|
|
|
|
}) async {
|
|
|
|
|
// Scudo anti-crash per lo sviluppo su Linux / Windows
|
|
|
|
|
if (!kIsWeb &&
|
|
|
|
|
!Platform.isAndroid &&
|
|
|
|
|
!Platform.isIOS &&
|
|
|
|
|
!Platform.isMacOS) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
final messaging = FirebaseMessaging.instance;
|
|
|
|
|
|
|
|
|
|
// 1. Richiesta permessi di notifica
|
|
|
|
|
final settings = await messaging.requestPermission(
|
|
|
|
|
alert: true,
|
|
|
|
|
badge: true,
|
|
|
|
|
sound: true,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
|
2026-06-01 10:55:28 +02:00
|
|
|
String? fcmToken;
|
|
|
|
|
if (kIsWeb) {
|
|
|
|
|
fcmToken = await messaging.getToken(
|
|
|
|
|
vapidKey:
|
|
|
|
|
'BLMUr7crlRghEW6iWtRZ7Y0a74OPAMG9Oh37ewhVP3_5YD9e5RHUeO79sDys6P-7KjOz6I6HiaPqNndmatQlu3g',
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
fcmToken = await messaging.getToken();
|
|
|
|
|
}
|
2026-05-30 12:12:14 +02:00
|
|
|
|
|
|
|
|
if (fcmToken != null) {
|
|
|
|
|
final supabase = GetIt.I.get<SupabaseClient>();
|
|
|
|
|
|
|
|
|
|
// Determiniamo la piattaforma in modo sicuro per Linux
|
|
|
|
|
String osPlatform = 'web';
|
|
|
|
|
if (!kIsWeb) {
|
|
|
|
|
if (Platform.isAndroid) osPlatform = 'android';
|
|
|
|
|
if (Platform.isIOS) osPlatform = 'ios';
|
|
|
|
|
if (Platform.isMacOS) osPlatform = 'macos';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. UPSERT su Supabase condizionato dal vincolo 'fcm_token'
|
|
|
|
|
await supabase.from('staff_devices').upsert(
|
|
|
|
|
{
|
|
|
|
|
'company_id': companyId,
|
|
|
|
|
'staff_id': staffId,
|
|
|
|
|
'fcm_token': fcmToken,
|
|
|
|
|
'os_platform': osPlatform,
|
|
|
|
|
'updated_at': DateTime.now().toIso8601String(),
|
|
|
|
|
},
|
|
|
|
|
onConflict:
|
|
|
|
|
'fcm_token', // Se il token esiste già, aggiorna questa riga!
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
debugPrint(
|
|
|
|
|
'Dispositivo registrato con successo su FLUX Cloud. Platform: $osPlatform',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
debugPrint('Permesso push negato dall\'utente.');
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
debugPrint('Errore durante la registrazione del dispositivo: $e');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-08 12:28:14 +02:00
|
|
|
void updateCurrentCompany(CompanyModel newCompany) {
|
|
|
|
|
emit(state.copyWith(company: newCompany));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 11:06:02 +02:00
|
|
|
// --- 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!
|
|
|
|
|
}
|
2026-04-26 10:15:34 +02:00
|
|
|
|
|
|
|
|
void setIsMobileDevice(bool isMobile) {
|
|
|
|
|
emit(state.copyWith(isMobileDevice: isMobile));
|
|
|
|
|
}
|
2026-05-13 12:41:07 +02:00
|
|
|
|
|
|
|
|
void setIsSingleUserMode(bool isSingleUser) {
|
|
|
|
|
emit(state.copyWith(isSingleUserMode: isSingleUser));
|
|
|
|
|
}
|
2026-04-22 11:06:02 +02:00
|
|
|
}
|