cid_app/lib/scambio_dati_screen.dart

473 lines
18 KiB
Dart
Raw Normal View History

2026-02-27 23:26:13 +01:00
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'global_data.dart';
import 'cid_data_manager.dart';
import 'security_service.dart';
class ScambioDatiScreen extends StatefulWidget {
const ScambioDatiScreen({super.key});
@override
State<ScambioDatiScreen> createState() => _ScambioDatiScreenState();
}
class _ScambioDatiScreenState extends State<ScambioDatiScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
final MobileScannerController _cameraController = MobileScannerController();
final TextEditingController _manualCodeController = TextEditingController();
String? _codiceSessione;
String? _chiaveSegreta;
String? _shortCode; // IL PIN DI 6 LETTERE
StreamSubscription? _streamSubscription;
bool _isLoading = false;
bool _scambioConcluso = false;
@override
void initState() {
super.initState();
2026-03-04 22:00:01 +01:00
// 🧹 AVVIO DEL NETTURBINO (con 10% di probabilità per non intasare il server)
if (Random().nextInt(10) == 0) {
_pulisciVecchieSessioni();
}
// RIMOZIONE DELLA SECONDA CHIAMATA LIBERA
2026-02-27 23:26:13 +01:00
// LOGICA DI APERTURA INTELLIGENTE:
// Lato A -> Inizia con la Fotocamera (Indice 1)
// Lato B -> Inizia mostrando il QR (Indice 0)
int initialIndex = (GlobalData.latoCorrente == 'A') ? 1 : 0;
_tabController = TabController(length: 2, vsync: this, initialIndex: initialIndex);
// Avvio automatico Host (prepara il QR in background a prescindere dalla Tab aperta)
_avviaHost();
}
2026-03-04 22:00:01 +01:00
// --- 🧹 IL NETTURBINO (GARBAGE COLLECTOR) ---
// Cerca nel database le sessioni più vecchie di 2 ore e le distrugge
Future<void> _pulisciVecchieSessioni() async {
try {
final dueOreFa = DateTime.now().subtract(const Duration(hours: 2));
final snapshot = await FirebaseFirestore.instance
.collection('scambi_cid')
.where('timestamp_scambio', isLessThan: Timestamp.fromDate(dueOreFa))
.get();
int cancellati = 0;
for (var doc in snapshot.docs) {
await doc.reference.delete();
cancellati++;
}
debugPrint("🧹 [NETTURBINO] Pulizia completata: eliminate $cancellati sessioni vecchie.");
} catch (e) {
debugPrint("⚠️ [NETTURBINO] Errore durante la pulizia: $e");
}
}
2026-02-27 23:26:13 +01:00
@override
void dispose() {
// Cancello la sessione se esco senza concludere, a meno che non sia l'ID della firma
if (!_scambioConcluso && _codiceSessione != null && _codiceSessione != GlobalData.idSessione) {
FirebaseFirestore.instance.collection('scambi_cid').doc(_codiceSessione).delete();
}
_streamSubscription?.cancel();
_tabController.dispose();
_cameraController.dispose();
_manualCodeController.dispose();
super.dispose();
}
// --- GENERATORE DI PIN A 6 LETTERE MAIUSCOLE ---
String _generaShortCode() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
Random rnd = Random();
return String.fromCharCodes(Iterable.generate(6, (_) => chars.codeUnitAt(rnd.nextInt(chars.length))));
}
// --- 1. LOGICA HOST (CHI MOSTRA IL QR) ---
Future<void> _avviaHost() async {
if (_codiceSessione != null) return;
if ((GlobalData.latoCorrente == 'A' && GlobalData.puntiFirmaA.isEmpty) ||
(GlobalData.latoCorrente == 'B' && GlobalData.puntiFirmaB.isEmpty)) {
return;
}
setState(() => _isLoading = true);
try {
String sessionId = GlobalData.idSessione ?? "CID_${DateTime.now().millisecondsSinceEpoch}";
String shortCode = _generaShortCode();
GlobalData.idSessione = sessionId;
GlobalData.idScambioTemporaneo = sessionId;
String chiave = GlobalData.chiaveSegretaCorrente ?? SecurityService.generaChiaveSessione();
GlobalData.chiaveSegretaCorrente = chiave;
Map<String, dynamic> mieiDati = CidDataManager.estraiDatiPerExport();
String payloadCriptato = SecurityService.criptaDati(mieiDati, chiave);
await FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId).set({
"timestamp_scambio": FieldValue.serverTimestamp(),
"host_lato": GlobalData.latoCorrente,
"secure_payload_host": payloadCriptato,
"secure_payload_guest": null,
"status": "waiting_guest",
"short_code": shortCode,
"chiave_temporanea": chiave
}, SetOptions(merge: true));
if (mounted) {
setState(() {
_codiceSessione = sessionId;
_chiaveSegreta = chiave;
_shortCode = shortCode;
_isLoading = false;
});
}
// Ascolto risposta dell'altro telefono
_streamSubscription = FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId).snapshots().listen((snapshot) {
if (!snapshot.exists) {
if (mounted) setState(() => _codiceSessione = null);
return;
}
var data = snapshot.data();
if (data != null && data['secure_payload_guest'] != null) {
_streamSubscription?.cancel();
_completaSync(data['secure_payload_guest'], chiave);
}
});
} catch (e) {
debugPrint("Err Host: $e");
if (mounted) setState(() => _isLoading = false);
}
}
void _completaSync(String encryptedData, String chiave) {
try {
Map<String, dynamic> dati = SecurityService.decriptaDati(encryptedData, chiave);
CidDataManager.importaDati(dati);
_scambioConcluso = true;
_showSuccessAndExit();
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Errore decriptazione")));
}
}
// --- 2. LOGICA GUEST: VIA FOTOCAMERA ---
void _onDetect(BarcodeCapture capture) {
if (_isLoading || _scambioConcluso) return;
final List<Barcode> barcodes = capture.barcodes;
for (final barcode in barcodes) {
if (barcode.rawValue != null) {
_partecipaGuestScansione(barcode.rawValue!);
break;
}
}
}
Future<void> _partecipaGuestScansione(String qrRawData) async {
if (qrRawData.isEmpty) return;
setState(() => _isLoading = true);
try {
if (!qrRawData.contains('|')) throw Exception("QR non valido");
var parts = qrRawData.split('|');
String sessionId = parts[0];
String chiave = parts[1];
await _eseguiPartecipazione(sessionId, chiave);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Errore Scansione: $e")));
await Future.delayed(const Duration(seconds: 2));
setState(() => _isLoading = false);
}
}
}
// --- 3. LOGICA GUEST: INSERIMENTO MANUALE ---
Future<void> _partecipaGuestManuale() async {
String codiceInserito = _manualCodeController.text.trim().toUpperCase();
if (codiceInserito.length < 5) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Inserisci un codice valido!"), backgroundColor: Colors.orange));
return;
}
setState(() => _isLoading = true);
try {
// 1. Cerca la sessione tramite codice corto su Firebase
final querySnapshot = await FirebaseFirestore.instance
.collection('scambi_cid')
.where('short_code', isEqualTo: codiceInserito)
.limit(1)
.get();
if (querySnapshot.docs.isEmpty) {
throw Exception("Codice non trovato o scaduto.");
}
final doc = querySnapshot.docs.first;
String sessionId = doc.id;
String chiave = doc['chiave_temporanea'] ?? "CHIAVE_DI_BACKUP";
await _eseguiPartecipazione(sessionId, chiave, manualHostData: doc.data());
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Errore: $e"), backgroundColor: Colors.red));
setState(() => _isLoading = false);
}
}
}
// --- FUNZIONE CENTRALE PER GUEST (USATA DA FOTOCAMERA E MANUALE) ---
Future<void> _eseguiPartecipazione(String sessionId, String chiave, {Map<String, dynamic>? manualHostData}) async {
if (GlobalData.idSessione != null && GlobalData.idSessione != sessionId) {
debugPrint("🗑️ Cancello vecchio file firma orfano: ${GlobalData.idSessione}");
await FirebaseFirestore.instance.collection('scambi_cid').doc(GlobalData.idSessione).delete();
GlobalData.idSessione = null;
}
Map<String, dynamic> data;
if (manualHostData != null) {
data = manualHostData;
} else {
DocumentSnapshot doc = await FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId).get();
if (!doc.exists) throw Exception("Sessione scaduta.");
data = doc.data() as Map<String, dynamic>;
}
String? hostDataEnc = data['secure_payload_host'];
if (hostDataEnc == null) throw Exception("Dati host mancanti.");
Map<String, dynamic> datiHost = SecurityService.decriptaDati(hostDataEnc, chiave);
CidDataManager.importaDati(datiHost);
Map<String, dynamic> mieiDati = CidDataManager.estraiDatiPerExport();
String myDataEnc = SecurityService.criptaDati(mieiDati, chiave);
await FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId).update({
"secure_payload_guest": myDataEnc,
"status": "completed"
});
GlobalData.idSessione = sessionId;
GlobalData.idScambioTemporaneo = sessionId;
GlobalData.chiaveSegretaCorrente = chiave;
_scambioConcluso = true;
_showSuccessAndExit();
}
void _showSuccessAndExit() {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("SCAMBIO EFFETTUATO!"), backgroundColor: Colors.green, duration: Duration(seconds: 1))
);
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
String qrString = "";
if (_codiceSessione != null && _chiaveSegreta != null) {
qrString = "$_codiceSessione|$_chiaveSegreta";
}
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text("Scambio Dati"),
backgroundColor: Colors.blue.shade900,
foregroundColor: Colors.white,
bottom: TabBar(
controller: _tabController,
indicatorColor: Colors.white,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
tabs: const [
Tab(icon: Icon(Icons.qr_code), text: "IL TUO QR"),
Tab(icon: Icon(Icons.camera_alt), text: "SCANSIONA / CODICE")
]
),
),
body: TabBarView(
controller: _tabController,
// Impedisce di strusciare accidentalmente e chiudere lo scanner
physics: const NeverScrollableScrollPhysics(),
children: [
// ================= TAB 1: HOST (MOSTRA QR E PIN) =================
Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_codiceSessione == null) ...[
const CircularProgressIndicator(),
const SizedBox(height: 20),
const Text("Generazione in corso...")
] else ...[
const Text("Fai scansionare questo QR:", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.white, border: Border.all(color: Colors.black12)),
child: QrImageView(data: qrString, size: 240)
),
const SizedBox(height: 30),
const Text("Oppure fai inserire questo PIN:", style: TextStyle(color: Colors.black54)),
const SizedBox(height: 5),
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.orange.shade800, width: 2)
),
child: Text(
_shortCode ?? "---",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 32, letterSpacing: 6, color: Colors.orange.shade900)
),
),
const SizedBox(height: 30),
const CircularProgressIndicator(),
const SizedBox(height: 10),
const Text("In attesa dell'altro utente...", style: TextStyle(color: Colors.grey))
]
]
)
)
),
// ================= TAB 2: GUEST (SCANNER SPLIT SCREEN) =================
Stack(
children: [
Column(
children: [
// METÀ SUPERIORE: FOTOCAMERA
Expanded(
flex: 5,
child: Stack(
children: [
MobileScanner(
controller: _cameraController,
onDetect: _onDetect,
),
// Overlay mirino per far capire all'utente dove inquadrare
Center(
child: Container(
width: 200, height: 200,
decoration: BoxDecoration(
border: Border.all(color: Colors.greenAccent, width: 3),
borderRadius: BorderRadius.circular(20)
),
),
),
Positioned(
bottom: 10,
left: 0,
right: 0,
child: Container(
color: Colors.black54,
padding: const EdgeInsets.symmetric(vertical: 8),
child: const Text(
"Inquadra il QR Code",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
)
)
],
),
),
// DIVISORIO
Container(height: 5, color: Colors.blue.shade900),
// METÀ INFERIORE: INSERIMENTO MANUALE
Expanded(
flex: 5,
child: Container(
color: Colors.grey.shade100,
width: double.infinity,
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.keyboard, size: 40, color: Colors.blueGrey),
const SizedBox(height: 10),
const Text(
"La fotocamera non funziona?\nInserisci qui il PIN a 6 lettere dell'altro utente:",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87),
),
const SizedBox(height: 20),
TextField(
controller: _manualCodeController,
textCapitalization: TextCapitalization.characters,
maxLength: 6,
textAlign: TextAlign.center,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24, letterSpacing: 8),
decoration: InputDecoration(
hintText: "PIN",
counterText: "",
border: OutlineInputBorder(borderRadius: BorderRadius.circular(15)),
filled: true,
fillColor: Colors.white,
prefixIcon: const Icon(Icons.password, color: Colors.blueGrey),
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange.shade800,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
),
onPressed: _partecipaGuestManuale,
icon: const Icon(Icons.download),
label: const Text("SCARICA DATI", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
),
)
],
),
),
),
)
],
),
// OVERLAY CARICAMENTO
if (_isLoading) const ColoredBox(color: Colors.black54, child: Center(child: CircularProgressIndicator(color: Colors.white))),
],
)
],
),
);
}
2026-03-04 23:00:01 +01:00
}