Auto-sync: 20260428_150000
This commit is contained in:
parent
94986ffd89
commit
3f7eeda2d6
12 changed files with 598 additions and 7 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
BIN
assets/certificate.pfx
Normal file
BIN
assets/certificate.pfx
Normal file
Binary file not shown.
|
|
@ -16,7 +16,7 @@ import 'main.dart';
|
||||||
import 'comp_6-7.dart';
|
import 'comp_6-7.dart';
|
||||||
import 'comp_1-5.dart';
|
import 'comp_1-5.dart';
|
||||||
import 'screens/paywall_screen.dart';
|
import 'screens/paywall_screen.dart';
|
||||||
|
import 'screens/fea_verification_screen.dart';
|
||||||
class Comp16Screen extends StatefulWidget {
|
class Comp16Screen extends StatefulWidget {
|
||||||
const Comp16Screen({super.key});
|
const Comp16Screen({super.key});
|
||||||
|
|
||||||
|
|
@ -295,10 +295,62 @@ class _Comp16ScreenState extends State<Comp16Screen> with WidgetsBindingObserver
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _ioApprovo() async {
|
Future<void> _ioApprovo() async {
|
||||||
|
// Chiudiamo l'anteprima se aperta
|
||||||
|
Navigator.of(context).popUntil((route) => route.isFirst || route.settings.name == null);
|
||||||
|
|
||||||
|
bool isA = GlobalData.latoCorrente == 'A';
|
||||||
|
|
||||||
|
// Telefoni dei due conducenti
|
||||||
|
String telA = GlobalData.N_tel_mail_cond_A.isNotEmpty ? GlobalData.N_tel_mail_cond_A : GlobalData.N_telefono_mail_contraente_A;
|
||||||
|
String telB = GlobalData.N_tel_mail_cond_B.isNotEmpty ? GlobalData.N_tel_mail_cond_B : GlobalData.N_telefono_mail_contraente_B;
|
||||||
|
|
||||||
|
Future<bool> eseguiVerifica(String tel, String nome, Function(String, String) onSuccess) async {
|
||||||
|
if (tel.isEmpty) return true; // Se non c'è numero (improbabile), skippa
|
||||||
|
bool verified = false;
|
||||||
|
await FeaVerificationModal.show(
|
||||||
|
context: context,
|
||||||
|
phoneNumber: tel,
|
||||||
|
conducenteName: nome,
|
||||||
|
onVerificationSuccess: () {
|
||||||
|
verified = true;
|
||||||
|
// Format data e ora
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
String dataOra = "${now.day.toString().padLeft(2,'0')}/${now.month.toString().padLeft(2,'0')}/${now.year} ${now.hour.toString().padLeft(2,'0')}:${now.minute.toString().padLeft(2,'0')}:${now.second.toString().padLeft(2,'0')}";
|
||||||
|
onSuccess(dataOra, "otp-${now.millisecondsSinceEpoch}");
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
onCancel: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return verified;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flusso OTP per A
|
||||||
|
if (telA.isNotEmpty) {
|
||||||
|
String nomeA = "${GlobalData.Nome_cond_A} ${GlobalData.Cognome_cond_A}".trim();
|
||||||
|
if (nomeA.isEmpty) nomeA = "${GlobalData.Nome_contraente_A} ${GlobalData.Cognome_contraente_A}".trim();
|
||||||
|
bool okA = await eseguiVerifica(telA, "Conducente A: $nomeA", (dataOra, id) {
|
||||||
|
GlobalData.feaVerifiedA = true; GlobalData.otpDataOraA = dataOra; GlobalData.otpIdA = id;
|
||||||
|
});
|
||||||
|
if (!okA) return; // Utente ha annullato
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flusso OTP per B
|
||||||
|
if (telB.isNotEmpty) {
|
||||||
|
String nomeB = "${GlobalData.Nome_cond_B} ${GlobalData.Cognome_cond_B}".trim();
|
||||||
|
if (nomeB.isEmpty) nomeB = "${GlobalData.Nome_contraente_B} ${GlobalData.Cognome_contraente_B}".trim();
|
||||||
|
bool okB = await eseguiVerifica(telB, "Conducente B: $nomeB", (dataOra, id) {
|
||||||
|
GlobalData.feaVerifiedB = true; GlobalData.otpDataOraB = dataOra; GlobalData.otpIdB = id;
|
||||||
|
});
|
||||||
|
if (!okB) return; // Utente ha annullato
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminate le verifiche, aggiorna Firebase e genera PDF definitivo sigillato
|
||||||
String? id = GlobalData.idSessione ?? GlobalData.idScambioTemporaneo;
|
String? id = GlobalData.idSessione ?? GlobalData.idScambioTemporaneo;
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
try {
|
try {
|
||||||
String field = (GlobalData.latoCorrente == 'A') ? 'approved_A' : 'approved_B';
|
String field = (isA) ? 'approved_A' : 'approved_B';
|
||||||
await FirebaseFirestore.instance.collection('scambi_cid').doc(id).update({field: true});
|
await FirebaseFirestore.instance.collection('scambi_cid').doc(id).update({field: true});
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
@ -307,6 +359,8 @@ class _Comp16ScreenState extends State<Comp16Screen> with WidgetsBindingObserver
|
||||||
setState(() {
|
setState(() {
|
||||||
_ioHoApprovato = true;
|
_ioHoApprovato = true;
|
||||||
});
|
});
|
||||||
|
// Rigenera i documenti per applicare l'attestato FEA e il sigillo PKCS12
|
||||||
|
await _generaDocumenti();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,14 @@ class GlobalData {
|
||||||
static String Cognome_cond_B = ""; static String Nome_cond_B = ""; static String Data_nascita_cond_B = ""; static String Cod_fiscale_cond_B = ""; static String Indirizzo_cond_B = ""; static String Stato_cond_B = ""; static String N_tel_mail_cond_B = ""; static String N_Patente_cond_B = ""; static String Scadenza_cond_B = ""; static String Categoria_cond_B = "";
|
static String Cognome_cond_B = ""; static String Nome_cond_B = ""; static String Data_nascita_cond_B = ""; static String Cod_fiscale_cond_B = ""; static String Indirizzo_cond_B = ""; static String Stato_cond_B = ""; static String N_tel_mail_cond_B = ""; static String N_Patente_cond_B = ""; static String Scadenza_cond_B = ""; static String Categoria_cond_B = "";
|
||||||
static List<String> puntiUrtoB_List = []; static String danni_visibili_B = ""; static String osservazioni_B = ""; static Map<int, bool> circostanzeB = {}; static int totaleCrocetteB = 0; static List<Offset?> puntiFirmaB = [];
|
static List<String> puntiUrtoB_List = []; static String danni_visibili_B = ""; static String osservazioni_B = ""; static Map<int, bool> circostanzeB = {}; static int totaleCrocetteB = 0; static List<Offset?> puntiFirmaB = [];
|
||||||
|
|
||||||
|
// --- DATI FEA (Firma Elettronica Avanzata) ---
|
||||||
|
static bool feaVerifiedA = false;
|
||||||
|
static String otpDataOraA = "";
|
||||||
|
static String otpIdA = "";
|
||||||
|
|
||||||
|
static bool feaVerifiedB = false;
|
||||||
|
static String otpDataOraB = "";
|
||||||
|
static String otpIdB = "";
|
||||||
// --- DATI GRAFICI ---
|
// --- DATI GRAFICI ---
|
||||||
static List<dynamic> tratti = [];
|
static List<dynamic> tratti = [];
|
||||||
static List<dynamic> elementi = [];
|
static List<dynamic> elementi = [];
|
||||||
|
|
@ -142,6 +150,7 @@ class GlobalData {
|
||||||
Denominazione_A = ""; Numero_Polizza_A = ""; N_carta_verde_A = ""; Data_Inizio_Dal_A = ""; Data_Scadenza_Al_A = ""; Agenzia_A = ""; Denominazione_agenzia_A = ""; Indirizzo_agenzia_A = ""; Stato_agenzia_A = ""; N_tel_mail_agenzia_A = ""; FLAG_danni_mat_assicurati_A = false;
|
Denominazione_A = ""; Numero_Polizza_A = ""; N_carta_verde_A = ""; Data_Inizio_Dal_A = ""; Data_Scadenza_Al_A = ""; Agenzia_A = ""; Denominazione_agenzia_A = ""; Indirizzo_agenzia_A = ""; Stato_agenzia_A = ""; N_tel_mail_agenzia_A = ""; FLAG_danni_mat_assicurati_A = false;
|
||||||
Cognome_cond_A = ""; Nome_cond_A = ""; Data_nascita_cond_A = ""; Cod_fiscale_cond_A = ""; Indirizzo_cond_A = ""; Stato_cond_A = ""; N_tel_mail_cond_A = ""; N_Patente_cond_A = ""; Scadenza_cond_A = ""; Categoria_cond_A = "";
|
Cognome_cond_A = ""; Nome_cond_A = ""; Data_nascita_cond_A = ""; Cod_fiscale_cond_A = ""; Indirizzo_cond_A = ""; Stato_cond_A = ""; N_tel_mail_cond_A = ""; N_Patente_cond_A = ""; Scadenza_cond_A = ""; Categoria_cond_A = "";
|
||||||
puntiUrtoA_List = []; danni_visibili_A = ""; osservazioni_A = ""; circostanzeA = {}; totaleCrocetteA = 0; puntiFirmaA = [];
|
puntiUrtoA_List = []; danni_visibili_A = ""; osservazioni_A = ""; circostanzeA = {}; totaleCrocetteA = 0; puntiFirmaA = [];
|
||||||
|
feaVerifiedA = false; otpDataOraA = ""; otpIdA = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
static void resetB() {
|
static void resetB() {
|
||||||
|
|
@ -150,6 +159,7 @@ class GlobalData {
|
||||||
Denominazione_B = ""; Numero_Polizza_B = ""; N_carta_verde_B = ""; Data_Inizio_Dal_B = ""; Data_Scadenza_Al_B = ""; Agenzia_B = ""; Denominazione_agenzia_B = ""; Indirizzo_agenzia_B = ""; Stato_agenzia_B = ""; N_tel_mail_agenzia_B = ""; FLAG_danni_mat_assicurati_B = false;
|
Denominazione_B = ""; Numero_Polizza_B = ""; N_carta_verde_B = ""; Data_Inizio_Dal_B = ""; Data_Scadenza_Al_B = ""; Agenzia_B = ""; Denominazione_agenzia_B = ""; Indirizzo_agenzia_B = ""; Stato_agenzia_B = ""; N_tel_mail_agenzia_B = ""; FLAG_danni_mat_assicurati_B = false;
|
||||||
Cognome_cond_B = ""; Nome_cond_B = ""; Data_nascita_cond_B = ""; Cod_fiscale_cond_B = ""; Indirizzo_cond_B = ""; Stato_cond_B = ""; N_tel_mail_cond_B = ""; N_Patente_cond_B = ""; Scadenza_cond_B = ""; Categoria_cond_B = "";
|
Cognome_cond_B = ""; Nome_cond_B = ""; Data_nascita_cond_B = ""; Cod_fiscale_cond_B = ""; Indirizzo_cond_B = ""; Stato_cond_B = ""; N_tel_mail_cond_B = ""; N_Patente_cond_B = ""; Scadenza_cond_B = ""; Categoria_cond_B = "";
|
||||||
puntiUrtoB_List = []; danni_visibili_B = ""; osservazioni_B = ""; circostanzeB = {}; totaleCrocetteB = 0; puntiFirmaB = [];
|
puntiUrtoB_List = []; danni_visibili_B = ""; osservazioni_B = ""; circostanzeB = {}; totaleCrocetteB = 0; puntiFirmaB = [];
|
||||||
|
feaVerifiedB = false; otpDataOraB = ""; otpIdB = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- DEBUG COMPLETO (Tutti i campi popolati) ---
|
// --- DEBUG COMPLETO (Tutti i campi popolati) ---
|
||||||
|
|
|
||||||
|
|
@ -113,19 +113,44 @@ class PdfEngine {
|
||||||
await _disegnaInBox(page, mappaCampi, CaiMapping.box_firma_B, await _renderFirmaTight(GlobalData.puntiFirmaB, Colors.black));
|
await _disegnaInBox(page, mappaCampi, CaiMapping.box_firma_B, await _renderFirmaTight(GlobalData.puntiFirmaB, Colors.black));
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
// SALVATAGGIO SICURO
|
// AGGIUNTA PAGINA 2 (FEA)
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
await _aggiungiPaginaFea(document);
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// FLATTENING E SALVATAGGIO TEMPORANEO
|
||||||
|
// =================================================================
|
||||||
|
try {
|
||||||
|
document.form.flattenAllFields();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("⚠️ Errore Flattening: $e");
|
||||||
|
document.form.readOnly = true;
|
||||||
|
}
|
||||||
|
|
||||||
List<int> bytesTemporanei = await document.save();
|
List<int> bytesTemporanei = await document.save();
|
||||||
document.dispose();
|
document.dispose();
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// APPOSIZIONE SIGILLO CRITTOGRAFICO (FEA)
|
||||||
|
// =================================================================
|
||||||
PdfDocument docFinale = PdfDocument(inputBytes: bytesTemporanei);
|
PdfDocument docFinale = PdfDocument(inputBytes: bytesTemporanei);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
docFinale.form.flattenAllFields();
|
final ByteData certData = await rootBundle.load('assets/certificate.pfx');
|
||||||
|
final Uint8List certBytes = certData.buffer.asUint8List();
|
||||||
|
|
||||||
|
PdfSignature signature = PdfSignature(
|
||||||
|
certificate: PdfCertificate(certBytes, 'caifacile123'),
|
||||||
|
contactInfo: 'CAI Facile App',
|
||||||
|
locationInfo: GlobalData.luogo.isNotEmpty ? GlobalData.luogo : 'Italia',
|
||||||
|
reason: 'Firma Elettronica Avanzata (FEA) tramite verifica OTP',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Aggiungiamo un campo firma invisibile (bounds: 0,0,0,0) sulla prima pagina
|
||||||
|
PdfSignatureField signatureField = PdfSignatureField(docFinale.pages[0], 'FirmaFEA', bounds: const Rect.fromLTWH(0, 0, 0, 0), signature: signature);
|
||||||
|
docFinale.form.fields.add(signatureField);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("⚠️ Errore Flattening: $e");
|
debugPrint("⚠️ Certificato FEA non trovato o errore firma: $e");
|
||||||
docFinale.form.readOnly = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<int> bytesFinali = await docFinale.save();
|
List<int> bytesFinali = await docFinale.save();
|
||||||
|
|
@ -139,6 +164,127 @@ class PdfEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// CREAZIONE ATTESTATO FEA (PAGINA 2)
|
||||||
|
// =================================================================
|
||||||
|
static Future<void> _aggiungiPaginaFea(PdfDocument document) async {
|
||||||
|
PdfPage page = document.pages.add();
|
||||||
|
final Size pageSize = page.getClientSize();
|
||||||
|
|
||||||
|
final PdfFont titleFont = PdfStandardFont(PdfFontFamily.helvetica, 16, style: PdfFontStyle.bold);
|
||||||
|
final PdfFont subTitleFont = PdfStandardFont(PdfFontFamily.helvetica, 9, style: PdfFontStyle.italic);
|
||||||
|
final PdfFont boldFont = PdfStandardFont(PdfFontFamily.helvetica, 11, style: PdfFontStyle.bold);
|
||||||
|
final PdfFont regularFont = PdfStandardFont(PdfFontFamily.helvetica, 10);
|
||||||
|
|
||||||
|
double y = 0;
|
||||||
|
|
||||||
|
String title = "ATTESTATO DI FIRMA ELETTRONICA AVANZATA (FEA)";
|
||||||
|
page.graphics.drawString(title, titleFont, bounds: Rect.fromLTWH(0, y, pageSize.width, 30), format: PdfStringFormat(alignment: PdfTextAlignment.center));
|
||||||
|
y += 25;
|
||||||
|
|
||||||
|
String subtitle = "Ai sensi dell'art. 20 del D.Lgs. 7 marzo 2005, n. 82 (CAD) e del DPCM 22 Febbraio 2013.";
|
||||||
|
page.graphics.drawString(subtitle, subTitleFont, bounds: Rect.fromLTWH(0, y, pageSize.width, 30), format: PdfStringFormat(alignment: PdfTextAlignment.center));
|
||||||
|
y += 40;
|
||||||
|
|
||||||
|
page.graphics.drawLine(PdfPen(PdfColor(100, 100, 100), width: 1), Offset(0, y), Offset(pageSize.width, y));
|
||||||
|
y += 20;
|
||||||
|
|
||||||
|
double colWidth = (pageSize.width / 2) - 10;
|
||||||
|
|
||||||
|
// Conducente A
|
||||||
|
double currentY = y;
|
||||||
|
page.graphics.drawString("VEICOLO A (Blu)", boldFont, bounds: Rect.fromLTWH(0, currentY, colWidth, 20), brush: PdfSolidBrush(PdfColor(0, 0, 200)));
|
||||||
|
currentY += 25;
|
||||||
|
|
||||||
|
String nomeA = "${GlobalData.Nome_cond_A} ${GlobalData.Cognome_cond_A}".trim();
|
||||||
|
if (nomeA.isEmpty) nomeA = "${GlobalData.Nome_contraente_A} ${GlobalData.Cognome_contraente_A}".trim();
|
||||||
|
String cfA = GlobalData.Cod_fiscale_cond_A.isNotEmpty ? GlobalData.Cod_fiscale_cond_A : GlobalData.Codice_Fiscale_contraente_A;
|
||||||
|
String telA = GlobalData.N_tel_mail_cond_A.isNotEmpty ? GlobalData.N_tel_mail_cond_A : GlobalData.N_telefono_mail_contraente_A;
|
||||||
|
|
||||||
|
page.graphics.drawString("Conducente: $nomeA", regularFont, bounds: Rect.fromLTWH(0, currentY, colWidth, 20));
|
||||||
|
currentY += 20;
|
||||||
|
page.graphics.drawString("Codice Fiscale: $cfA", regularFont, bounds: Rect.fromLTWH(0, currentY, colWidth, 20));
|
||||||
|
currentY += 20;
|
||||||
|
|
||||||
|
if (GlobalData.feaVerifiedA) {
|
||||||
|
page.graphics.drawString("Cellulare Verificato: $telA", boldFont, bounds: Rect.fromLTWH(0, currentY, colWidth, 20), brush: PdfSolidBrush(PdfColor(0, 128, 0)));
|
||||||
|
currentY += 20;
|
||||||
|
page.graphics.drawString("Data/Ora Verifica: ${GlobalData.otpDataOraA}", regularFont, bounds: Rect.fromLTWH(0, currentY, colWidth, 20));
|
||||||
|
currentY += 20;
|
||||||
|
page.graphics.drawString("ID Transazione OTP: ${GlobalData.otpIdA}", regularFont, bounds: Rect.fromLTWH(0, currentY, colWidth, 20));
|
||||||
|
} else {
|
||||||
|
page.graphics.drawString("Firma Elettronica Semplice (FES)", boldFont, bounds: Rect.fromLTWH(0, currentY, colWidth, 20), brush: PdfSolidBrush(PdfColor(200, 100, 0)));
|
||||||
|
currentY += 40;
|
||||||
|
}
|
||||||
|
currentY += 30;
|
||||||
|
page.graphics.drawString("Firma Autografa:", boldFont, bounds: Rect.fromLTWH(0, currentY, colWidth, 20));
|
||||||
|
currentY += 20;
|
||||||
|
page.graphics.drawRectangle(pen: PdfPen(PdfColor(200, 200, 200)), bounds: Rect.fromLTWH(0, currentY, colWidth, 80));
|
||||||
|
|
||||||
|
// Draw Firma A
|
||||||
|
Uint8List? firmaA = await _renderFirmaTight(GlobalData.puntiFirmaA, Colors.black);
|
||||||
|
if (firmaA != null) {
|
||||||
|
PdfBitmap bitmap = PdfBitmap(firmaA);
|
||||||
|
double scale = _getScale(bitmap.width.toDouble(), bitmap.height.toDouble(), colWidth - 10, 70);
|
||||||
|
page.graphics.drawImage(bitmap, Rect.fromLTWH(5, currentY + 5, bitmap.width * scale, bitmap.height * scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conducente B
|
||||||
|
currentY = y;
|
||||||
|
double startXB = pageSize.width / 2 + 10;
|
||||||
|
page.graphics.drawString("VEICOLO B (Giallo)", boldFont, bounds: Rect.fromLTWH(startXB, currentY, colWidth, 20), brush: PdfSolidBrush(PdfColor(200, 150, 0)));
|
||||||
|
currentY += 25;
|
||||||
|
|
||||||
|
String nomeB = "${GlobalData.Nome_cond_B} ${GlobalData.Cognome_cond_B}".trim();
|
||||||
|
if (nomeB.isEmpty) nomeB = "${GlobalData.Nome_contraente_B} ${GlobalData.Cognome_contraente_B}".trim();
|
||||||
|
String cfB = GlobalData.Cod_fiscale_cond_B.isNotEmpty ? GlobalData.Cod_fiscale_cond_B : GlobalData.Codice_Fiscale_contraente_B;
|
||||||
|
String telB = GlobalData.N_tel_mail_cond_B.isNotEmpty ? GlobalData.N_tel_mail_cond_B : GlobalData.N_telefono_mail_contraente_B;
|
||||||
|
|
||||||
|
page.graphics.drawString("Conducente: $nomeB", regularFont, bounds: Rect.fromLTWH(startXB, currentY, colWidth, 20));
|
||||||
|
currentY += 20;
|
||||||
|
page.graphics.drawString("Codice Fiscale: $cfB", regularFont, bounds: Rect.fromLTWH(startXB, currentY, colWidth, 20));
|
||||||
|
currentY += 20;
|
||||||
|
|
||||||
|
if (GlobalData.feaVerifiedB) {
|
||||||
|
page.graphics.drawString("Cellulare Verificato: $telB", boldFont, bounds: Rect.fromLTWH(startXB, currentY, colWidth, 20), brush: PdfSolidBrush(PdfColor(0, 128, 0)));
|
||||||
|
currentY += 20;
|
||||||
|
page.graphics.drawString("Data/Ora Verifica: ${GlobalData.otpDataOraB}", regularFont, bounds: Rect.fromLTWH(startXB, currentY, colWidth, 20));
|
||||||
|
currentY += 20;
|
||||||
|
page.graphics.drawString("ID Transazione OTP: ${GlobalData.otpIdB}", regularFont, bounds: Rect.fromLTWH(startXB, currentY, colWidth, 20));
|
||||||
|
} else {
|
||||||
|
page.graphics.drawString("Firma Elettronica Semplice (FES)", boldFont, bounds: Rect.fromLTWH(startXB, currentY, colWidth, 20), brush: PdfSolidBrush(PdfColor(200, 100, 0)));
|
||||||
|
currentY += 40;
|
||||||
|
}
|
||||||
|
currentY += 30;
|
||||||
|
page.graphics.drawString("Firma Autografa:", boldFont, bounds: Rect.fromLTWH(startXB, currentY, colWidth, 20));
|
||||||
|
currentY += 20;
|
||||||
|
page.graphics.drawRectangle(pen: PdfPen(PdfColor(200, 200, 200)), bounds: Rect.fromLTWH(startXB, currentY, colWidth, 80));
|
||||||
|
|
||||||
|
// Draw Firma B
|
||||||
|
Uint8List? firmaB = await _renderFirmaTight(GlobalData.puntiFirmaB, Colors.black);
|
||||||
|
if (firmaB != null) {
|
||||||
|
PdfBitmap bitmap = PdfBitmap(firmaB);
|
||||||
|
double scale = _getScale(bitmap.width.toDouble(), bitmap.height.toDouble(), colWidth - 10, 70);
|
||||||
|
page.graphics.drawImage(bitmap, Rect.fromLTWH(startXB + 5, currentY + 5, bitmap.width * scale, bitmap.height * scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
y = currentY + 110;
|
||||||
|
|
||||||
|
page.graphics.drawLine(PdfPen(PdfColor(100, 100, 100), width: 1), Offset(0, y), Offset(pageSize.width, y));
|
||||||
|
y += 20;
|
||||||
|
String disclaimer = "Il presente documento è sigillato crittograficamente in modo inalterabile.\n"
|
||||||
|
"Qualsiasi modifica apportata al file dopo l'apposizione del sigillo invaliderà le firme.\n"
|
||||||
|
"Le identità dei firmatari (ove specificato 'Verificato') sono state accertate tramite associazione univoca "
|
||||||
|
"tra il numero di telefono cellulare e il codice OTP inserito sul dispositivo al momento della firma.";
|
||||||
|
page.graphics.drawString(disclaimer, subTitleFont, bounds: Rect.fromLTWH(0, y, pageSize.width, 100), format: PdfStringFormat(alignment: PdfTextAlignment.center));
|
||||||
|
}
|
||||||
|
|
||||||
|
static double _getScale(double imgW, double imgH, double boxW, double boxH) {
|
||||||
|
double ratioX = boxW / imgW;
|
||||||
|
double ratioY = boxH / imgH;
|
||||||
|
return (ratioX < ratioY) ? ratioX : ratioY;
|
||||||
|
}
|
||||||
|
|
||||||
// ... (Keep existing helper methods: _riempiCampoSplit, _scriviX, _scriviTesto, _scriviXRossa, _scriviTestoTotale, _disegnaInBox, _renderFirmaTight, _valoreDaGlobal)
|
// ... (Keep existing helper methods: _riempiCampoSplit, _scriviX, _scriviTesto, _scriviXRossa, _scriviTestoTotale, _disegnaInBox, _renderFirmaTight, _valoreDaGlobal)
|
||||||
// Re-pasting them here for completeness to ensure no missing dependencies
|
// Re-pasting them here for completeness to ensure no missing dependencies
|
||||||
|
|
||||||
|
|
|
||||||
210
lib/screens/fea_verification_screen.dart
Normal file
210
lib/screens/fea_verification_screen.dart
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
import 'dart:ui';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
import '../services/otp_service.dart';
|
||||||
|
|
||||||
|
class FeaVerificationModal extends StatefulWidget {
|
||||||
|
final String phoneNumber;
|
||||||
|
final String conducenteName;
|
||||||
|
final VoidCallback onVerificationSuccess;
|
||||||
|
final VoidCallback onCancel;
|
||||||
|
|
||||||
|
const FeaVerificationModal({
|
||||||
|
super.key,
|
||||||
|
required this.phoneNumber,
|
||||||
|
required this.conducenteName,
|
||||||
|
required this.onVerificationSuccess,
|
||||||
|
required this.onCancel,
|
||||||
|
});
|
||||||
|
|
||||||
|
static Future<void> show({
|
||||||
|
required BuildContext context,
|
||||||
|
required String phoneNumber,
|
||||||
|
required String conducenteName,
|
||||||
|
required VoidCallback onVerificationSuccess,
|
||||||
|
required VoidCallback onCancel,
|
||||||
|
}) {
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => FeaVerificationModal(
|
||||||
|
phoneNumber: phoneNumber,
|
||||||
|
conducenteName: conducenteName,
|
||||||
|
onVerificationSuccess: onVerificationSuccess,
|
||||||
|
onCancel: onCancel,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FeaVerificationModal> createState() => _FeaVerificationModalState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FeaVerificationModalState extends State<FeaVerificationModal> {
|
||||||
|
final OtpService _otpService = OtpService();
|
||||||
|
final TextEditingController _otpController = TextEditingController();
|
||||||
|
|
||||||
|
bool _isLoading = false;
|
||||||
|
bool _codeSent = false;
|
||||||
|
String? _verificationId;
|
||||||
|
String? _errorMessage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_inviaSms();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _inviaSms() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _otpService.sendOtp(
|
||||||
|
phoneNumber: widget.phoneNumber,
|
||||||
|
onCodeSent: (verId) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_verificationId = verId;
|
||||||
|
_codeSent = true;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onVerificationFailed: (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = e.message ?? "Errore nell'invio dell'SMS. Controlla il numero.";
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onVerificationCompleted: (credential) async {
|
||||||
|
// Auto-resolution (su alcuni Android)
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await FirebaseAuth.instance.signInWithCredential(credential);
|
||||||
|
widget.onVerificationSuccess();
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = "Errore auto-verifica.";
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCodeAutoRetrievalTimeout: (verId) {
|
||||||
|
_verificationId = verId;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = "Errore generico: $e";
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _verificaPin() async {
|
||||||
|
final code = _otpController.text.trim();
|
||||||
|
if (code.length != 6 || _verificationId == null) {
|
||||||
|
setState(() => _errorMessage = "Inserisci il codice a 6 cifre");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _otpService.verifyCode(verificationId: _verificationId!, smsCode: code);
|
||||||
|
widget.onVerificationSuccess();
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = "Codice errato o scaduto. Riprova.";
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||||
|
child: AlertDialog(
|
||||||
|
backgroundColor: Colors.white.withValues(alpha: 0.95),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
|
title: Column(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.verified_user, color: Colors.green, size: 50),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
const Text("Firma Elettronica", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), textAlign: TextAlign.center),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
Text(widget.conducenteName, style: const TextStyle(fontSize: 14, color: Colors.black87), textAlign: TextAlign.center),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (_errorMessage != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10),
|
||||||
|
child: Text(_errorMessage!, style: const TextStyle(color: Colors.red, fontSize: 13), textAlign: TextAlign.center),
|
||||||
|
),
|
||||||
|
if (!_codeSent) ...[
|
||||||
|
const Text("Invio SMS in corso...", textAlign: TextAlign.center),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
if (_isLoading) const CircularProgressIndicator(),
|
||||||
|
] else ...[
|
||||||
|
Text("Abbiamo inviato un codice a:\n${widget.phoneNumber}", textAlign: TextAlign.center),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
TextField(
|
||||||
|
controller: _otpController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
maxLength: 6,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 24, letterSpacing: 8, fontWeight: FontWeight.bold),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
counterText: "",
|
||||||
|
hintText: "000000",
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.grey.shade100,
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
if (_isLoading) const CircularProgressIndicator()
|
||||||
|
else ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.blue.shade800,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
minimumSize: const Size(double.infinity, 50),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
elevation: 2,
|
||||||
|
),
|
||||||
|
onPressed: _verificaPin,
|
||||||
|
child: const Text("VERIFICA", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: widget.onCancel,
|
||||||
|
child: const Text("ANNULLA", style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
lib/services/otp_service.dart
Normal file
71
lib/services/otp_service.dart
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
class OtpService {
|
||||||
|
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||||
|
|
||||||
|
String? _verificationId;
|
||||||
|
int? _resendToken;
|
||||||
|
|
||||||
|
/// Invia l'SMS al numero di telefono (deve avere il prefisso internazionale, es. +39).
|
||||||
|
Future<void> sendOtp({
|
||||||
|
required String phoneNumber,
|
||||||
|
required Function(String verificationId) onCodeSent,
|
||||||
|
required Function(FirebaseAuthException e) onVerificationFailed,
|
||||||
|
required Function(PhoneAuthCredential credential) onVerificationCompleted,
|
||||||
|
required Function(String verificationId) onCodeAutoRetrievalTimeout,
|
||||||
|
}) async {
|
||||||
|
// Normalizzazione numero (aggiunta +39 se manca)
|
||||||
|
String normalizedPhone = phoneNumber.trim().replaceAll(' ', '');
|
||||||
|
if (!normalizedPhone.startsWith('+')) {
|
||||||
|
if (normalizedPhone.startsWith('00')) {
|
||||||
|
normalizedPhone = '+' + normalizedPhone.substring(2);
|
||||||
|
} else {
|
||||||
|
// Presumo Italia come default
|
||||||
|
normalizedPhone = '+39' + normalizedPhone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint("Invio SMS a: $normalizedPhone");
|
||||||
|
|
||||||
|
await _auth.verifyPhoneNumber(
|
||||||
|
phoneNumber: normalizedPhone,
|
||||||
|
verificationCompleted: (PhoneAuthCredential credential) {
|
||||||
|
// Solo su Android, a volte legge in automatico l'SMS
|
||||||
|
onVerificationCompleted(credential);
|
||||||
|
},
|
||||||
|
verificationFailed: (FirebaseAuthException e) {
|
||||||
|
debugPrint("Verifica Fallita: ${e.message}");
|
||||||
|
onVerificationFailed(e);
|
||||||
|
},
|
||||||
|
codeSent: (String verificationId, int? resendToken) {
|
||||||
|
_verificationId = verificationId;
|
||||||
|
_resendToken = resendToken;
|
||||||
|
onCodeSent(verificationId);
|
||||||
|
},
|
||||||
|
codeAutoRetrievalTimeout: (String verificationId) {
|
||||||
|
_verificationId = verificationId;
|
||||||
|
onCodeAutoRetrievalTimeout(verificationId);
|
||||||
|
},
|
||||||
|
forceResendingToken: _resendToken,
|
||||||
|
timeout: const Duration(seconds: 60),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifica il codice PIN (OTP) inserito dall'utente.
|
||||||
|
/// Ritorna un UserCredential se la verifica ha successo, altrimenti lancia un'eccezione.
|
||||||
|
Future<UserCredential> verifyCode({required String verificationId, required String smsCode}) async {
|
||||||
|
PhoneAuthCredential credential = PhoneAuthProvider.credential(
|
||||||
|
verificationId: verificationId,
|
||||||
|
smsCode: smsCode,
|
||||||
|
);
|
||||||
|
// Firma e logga l'utente verificando l'OTP.
|
||||||
|
return await _auth.signInWithCredential(credential);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Effettua il logout per ripulire lo stato se necessario (es. dopo l'invio del CID)
|
||||||
|
Future<void> signOut() async {
|
||||||
|
await _auth.signOut();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Current State
|
# Current State
|
||||||
|
|
||||||
L'app "CAI Facile" è in uno stato avanzato di sviluppo. Lo scambio dati remoto e la generazione PDF sono attivi.
|
L'app "CAI Facile" è in uno stato avanzato di sviluppo. Lo scambio dati remoto, la monetizzazione e la generazione PDF sono attivi. Attualmente è in corso l'implementazione della Firma Elettronica Avanzata (FEA).
|
||||||
|
|
||||||
## Funzionalità Implementate
|
## Funzionalità Implementate
|
||||||
- UI/UX completa per le sezioni 1-15 del modulo CID.
|
- UI/UX completa per le sezioni 1-15 del modulo CID.
|
||||||
|
|
|
||||||
|
|
@ -17,3 +17,8 @@
|
||||||
## Monetizzazione & Paywall (RevenueCat)
|
## Monetizzazione & Paywall (RevenueCat)
|
||||||
- **Decisione**: L'app utilizza un modello "Hard Paywall parziale". Gli utenti possono compilare il modulo offline gratuitamente, ma la **funzionalità core di Scambio Dati (QR Code e sincronizzazione Firebase)** è bloccata dietro un abbonamento annuale.
|
- **Decisione**: L'app utilizza un modello "Hard Paywall parziale". Gli utenti possono compilare il modulo offline gratuitamente, ma la **funzionalità core di Scambio Dati (QR Code e sincronizzazione Firebase)** è bloccata dietro un abbonamento annuale.
|
||||||
- **Implementazione**: Utilizzo di RevenueCat (`purchases_flutter`) per delegare la validazione delle ricevute lato server. Interfaccia Paywall custom (`PaywallScreen`) piuttosto che il pacchetto UI di RevenueCat per mantenere coerenza stilistica e aumentare le conversioni con copy persuasivo ("Sblocca la sincronizzazione").
|
- **Implementazione**: Utilizzo di RevenueCat (`purchases_flutter`) per delegare la validazione delle ricevute lato server. Interfaccia Paywall custom (`PaywallScreen`) piuttosto che il pacchetto UI di RevenueCat per mantenere coerenza stilistica e aumentare le conversioni con copy persuasivo ("Sblocca la sincronizzazione").
|
||||||
|
|
||||||
|
## Firma Elettronica Avanzata (FEA)
|
||||||
|
- **Decisione**: Per conferire pieno valore legale di "scrittura privata" al documento (ex art. 2702 c.c.) e rispettare i requisiti del CAD, il CID viene protetto con una Firma Elettronica Avanzata.
|
||||||
|
- **Flusso**: 1) Identificazione utente tramite OTP via SMS (Firebase Phone Auth). 2) Aggiunta di un "Attestato di Certificazione FEA" in Pagina 2 del PDF con firme autografe visibili. 3) Applicazione di un sigillo crittografico invisibile sull'intero PDF tramite certificato PKCS#12 (`assets/certificate.pfx`).
|
||||||
|
- **Privacy (GDPR)**: Nessun log viene salvato sul database Firebase. La prova crittografica e l'Audit Trail dell'OTP "viaggiano" esclusivamente fusi all'interno del PDF stesso, in modo inalterabile.
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ flutter:
|
||||||
- assets/fonts/Roboto-Bold.ttf
|
- assets/fonts/Roboto-Bold.ttf
|
||||||
- assets/sfondo_cid.jpg
|
- assets/sfondo_cid.jpg
|
||||||
- assets/icona.png
|
- assets/icona.png
|
||||||
|
- assets/certificate.pfx
|
||||||
|
|
||||||
fonts:
|
fonts:
|
||||||
- family: Roboto
|
- family: Roboto
|
||||||
|
|
|
||||||
94
test/generate_fea_preview_test.dart
Normal file
94
test/generate_fea_preview_test.dart
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:syncfusion_flutter_pdf/pdf.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('Genera Preview Pagina 2 FEA', () async {
|
||||||
|
PdfDocument document = PdfDocument();
|
||||||
|
PdfPage page = document.pages.add();
|
||||||
|
final Size pageSize = page.getClientSize();
|
||||||
|
|
||||||
|
// Draw Header
|
||||||
|
final PdfFont titleFont = PdfStandardFont(PdfFontFamily.helvetica, 18, style: PdfFontStyle.bold);
|
||||||
|
final PdfFont subTitleFont = PdfStandardFont(PdfFontFamily.helvetica, 10, style: PdfFontStyle.italic);
|
||||||
|
final PdfFont boldFont = PdfStandardFont(PdfFontFamily.helvetica, 12, style: PdfFontStyle.bold);
|
||||||
|
final PdfFont regularFont = PdfStandardFont(PdfFontFamily.helvetica, 11);
|
||||||
|
|
||||||
|
double y = 0;
|
||||||
|
|
||||||
|
// Title
|
||||||
|
String title = "ATTESTATO DI FIRMA ELETTRONICA AVANZATA (FEA)";
|
||||||
|
page.graphics.drawString(title, titleFont,
|
||||||
|
bounds: Rect.fromLTWH(0, y, pageSize.width, 30),
|
||||||
|
format: PdfStringFormat(alignment: PdfTextAlignment.center));
|
||||||
|
y += 25;
|
||||||
|
|
||||||
|
String subtitle = "Ai sensi dell'art. 20 del D.Lgs. 7 marzo 2005, n. 82 (CAD) e del DPCM 22 Febbraio 2013.";
|
||||||
|
page.graphics.drawString(subtitle, subTitleFont,
|
||||||
|
bounds: Rect.fromLTWH(0, y, pageSize.width, 30),
|
||||||
|
format: PdfStringFormat(alignment: PdfTextAlignment.center));
|
||||||
|
y += 50;
|
||||||
|
|
||||||
|
// Draw separator
|
||||||
|
page.graphics.drawLine(PdfPen(PdfColor(100, 100, 100), width: 1), Offset(0, y), Offset(pageSize.width, y));
|
||||||
|
y += 20;
|
||||||
|
|
||||||
|
// Split into two columns for Conducente A and B
|
||||||
|
double colWidth = (pageSize.width / 2) - 20;
|
||||||
|
|
||||||
|
// Conducente A
|
||||||
|
double currentY = y;
|
||||||
|
page.graphics.drawString("VEICOLO A (Blu)", boldFont, bounds: Rect.fromLTWH(0, currentY, colWidth, 20), brush: PdfSolidBrush(PdfColor(0, 0, 200)));
|
||||||
|
currentY += 25;
|
||||||
|
page.graphics.drawString("Conducente: Mario Rossi", regularFont, bounds: Rect.fromLTWH(0, currentY, colWidth, 20));
|
||||||
|
currentY += 20;
|
||||||
|
page.graphics.drawString("Codice Fiscale: RSSMRA80A01H501U", regularFont, bounds: Rect.fromLTWH(0, currentY, colWidth, 20));
|
||||||
|
currentY += 20;
|
||||||
|
page.graphics.drawString("Cellulare Verificato: +39 333 1234567", boldFont, bounds: Rect.fromLTWH(0, currentY, colWidth, 20), brush: PdfSolidBrush(PdfColor(0, 128, 0)));
|
||||||
|
currentY += 20;
|
||||||
|
page.graphics.drawString("Data/Ora Verifica: 28/04/2026 14:35:12", regularFont, bounds: Rect.fromLTWH(0, currentY, colWidth, 20));
|
||||||
|
currentY += 20;
|
||||||
|
page.graphics.drawString("ID Transazione OTP: fb-otp-849201a", regularFont, bounds: Rect.fromLTWH(0, currentY, colWidth, 20));
|
||||||
|
currentY += 30;
|
||||||
|
page.graphics.drawString("Firma Elettronica Autografa:", boldFont, bounds: Rect.fromLTWH(0, currentY, colWidth, 20));
|
||||||
|
currentY += 20;
|
||||||
|
page.graphics.drawRectangle(pen: PdfPen(PdfColor(200, 200, 200)), bounds: Rect.fromLTWH(0, currentY, colWidth, 100));
|
||||||
|
page.graphics.drawString("[ Disegno della Firma A ]", subTitleFont, bounds: Rect.fromLTWH(0, currentY + 40, colWidth, 20), format: PdfStringFormat(alignment: PdfTextAlignment.center));
|
||||||
|
|
||||||
|
// Conducente B
|
||||||
|
currentY = y;
|
||||||
|
page.graphics.drawString("VEICOLO B (Giallo)", boldFont, bounds: Rect.fromLTWH(pageSize.width / 2 + 10, currentY, colWidth, 20), brush: PdfSolidBrush(PdfColor(200, 150, 0)));
|
||||||
|
currentY += 25;
|
||||||
|
page.graphics.drawString("Conducente: Luigi Verdi", regularFont, bounds: Rect.fromLTWH(pageSize.width / 2 + 10, currentY, colWidth, 20));
|
||||||
|
currentY += 20;
|
||||||
|
page.graphics.drawString("Codice Fiscale: VRDLGI90B02F205Z", regularFont, bounds: Rect.fromLTWH(pageSize.width / 2 + 10, currentY, colWidth, 20));
|
||||||
|
currentY += 20;
|
||||||
|
page.graphics.drawString("Cellulare Verificato: +39 340 9876543", boldFont, bounds: Rect.fromLTWH(pageSize.width / 2 + 10, currentY, colWidth, 20), brush: PdfSolidBrush(PdfColor(0, 128, 0)));
|
||||||
|
currentY += 20;
|
||||||
|
page.graphics.drawString("Data/Ora Verifica: 28/04/2026 14:36:05", regularFont, bounds: Rect.fromLTWH(pageSize.width / 2 + 10, currentY, colWidth, 20));
|
||||||
|
currentY += 20;
|
||||||
|
page.graphics.drawString("ID Transazione OTP: fb-otp-112399b", regularFont, bounds: Rect.fromLTWH(pageSize.width / 2 + 10, currentY, colWidth, 20));
|
||||||
|
currentY += 30;
|
||||||
|
page.graphics.drawString("Firma Elettronica Autografa:", boldFont, bounds: Rect.fromLTWH(pageSize.width / 2 + 10, currentY, colWidth, 20));
|
||||||
|
currentY += 20;
|
||||||
|
page.graphics.drawRectangle(pen: PdfPen(PdfColor(200, 200, 200)), bounds: Rect.fromLTWH(pageSize.width / 2 + 10, currentY, colWidth, 100));
|
||||||
|
page.graphics.drawString("[ Disegno della Firma B ]", subTitleFont, bounds: Rect.fromLTWH(pageSize.width / 2 + 10, currentY + 40, colWidth, 20), format: PdfStringFormat(alignment: PdfTextAlignment.center));
|
||||||
|
|
||||||
|
y = currentY + 150;
|
||||||
|
|
||||||
|
// Footer Disclaimer
|
||||||
|
page.graphics.drawLine(PdfPen(PdfColor(100, 100, 100), width: 1), Offset(0, y), Offset(pageSize.width, y));
|
||||||
|
y += 20;
|
||||||
|
String disclaimer = "Il presente documento è sigillato crittograficamente in modo inalterabile.\n"
|
||||||
|
"Qualsiasi modifica apportata al file dopo l'apposizione del sigillo invaliderà le firme.\n"
|
||||||
|
"Le identità dei firmatari sono state verificate tramite associazione univoca "
|
||||||
|
"tra il numero di telefono cellulare e il codice OTP inserito sul dispositivo al momento della firma.";
|
||||||
|
page.graphics.drawString(disclaimer, subTitleFont, bounds: Rect.fromLTWH(0, y, pageSize.width, 100), format: PdfStringFormat(alignment: PdfTextAlignment.center));
|
||||||
|
|
||||||
|
// Salva il file
|
||||||
|
final file = File('/Volumes/NVME-2TB/Sviluppo/development/cid_app/test/pagina_2_fea_preview.pdf');
|
||||||
|
file.writeAsBytesSync(await document.save());
|
||||||
|
document.dispose();
|
||||||
|
});
|
||||||
|
}
|
||||||
BIN
test/pagina_2_fea_preview.pdf
Normal file
BIN
test/pagina_2_fea_preview.pdf
Normal file
Binary file not shown.
Loading…
Reference in a new issue