Come creare un modulo di richiesta preventivo in Flutter
Un modulo di richiesta in Flutter con scelta del servizio, budget e scadenza che ti invia ogni richiesta via email e manda al cliente una conferma automatica, senza backend.
Aggiornato:
In questo tutorial crei in Flutter una schermata di richiesta preventivo: il cliente sceglie un servizio, un budget e una scadenza, descrive il progetto e tocca Send. Tu ricevi un’email ben formattata, il cliente una conferma automatica.
Ti serve: un account Frontmail gratuito, un progetto Flutter 3.24+ e una ventina di minuti.
1. Collega la tua casella e crea i modelli
- Servizi email → Aggiungi servizio: collega la tua casella o il tuo provider, seleziona Imposta come predefinito, poi Verifica e salva.
- Modelli → Dalla galleria → Richiesta di preventivo (categoria Business) → Usa modello.
- Aggiungi la conferma: Dalla galleria → Risposta automatica: grazie per il messaggio →
Usa modello, imposta nella sua scheda Parametri i valori predefiniti di
company_nameeresponse_time, poi selezionalo nel modello di preventivo in Impostazioni → Consegna → Modello di risposta automatica.
Il modello di preventivo si aspetta name, email, service e details (obbligatori) e phone,
company, budget, deadline (facoltativi). service e budget accettano solo i valori
elencati nella scheda Parametri: adatta gli elenchi a ciò che vendi. L’esempio di codice usa i
valori del modello inglese; se crei il modello in italiano, i valori sono Sito web, E-commerce,
App mobile, Consulenza, Altro e Meno di 5.000 €, 5.000–20.000 €, 20.000–50.000 €, Oltre
50.000 €. L’app deve inviare esattamente quelle stringhe.
2. Consenti le app mobile e attiva il CAPTCHA
- Sicurezza → App mobile → Consenti app mobile → Salva. Senza questa impostazione
le richieste prive dell’
Origindi un sito web vengono rifiutate conorigin_not_allowed. - Modello di preventivo → Impostazioni → Protezione → Richiedi CAPTCHA (Turnstile). Le app usano il widget condiviso di Frontmail: non serve un account Cloudflare.
3. Installa
flutter pub add frontmail webview_flutter url_launcher
Copia TurnstileView dalla sezione Turnstile della documentazione dell’SDK Flutter in
lib/turnstile_view.dart.
4. Crea il modulo
import 'package:flutter/material.dart';
import 'package:frontmail/frontmail.dart';
import 'turnstile_view.dart';
final frontmail = Frontmail(publicKey: 'pk_…');
const services = ['Website', 'E-shop', 'Mobile app', 'Consulting', 'Other'];
const budgets = ['Under \$5k', '\$5k–20k', '\$20k–50k', 'Over \$50k'];
class QuoteScreen extends StatefulWidget {
const QuoteScreen({super.key});
@override
State<QuoteScreen> createState() => _QuoteScreenState();
}
class _QuoteScreenState extends State<QuoteScreen> {
final _name = TextEditingController();
final _email = TextEditingController();
final _details = TextEditingController();
String _service = services.first;
String? _budget;
DateTime? _deadline;
TurnstileSite? _site;
String? _token;
int _resetId = 0;
String _info = '';
@override
void initState() {
super.initState();
Turnstile.site(client: frontmail).then((site) => setState(() => _site = site));
}
Future<void> _pickDeadline() async {
final now = DateTime.now();
final picked = await showDatePicker(context: context, firstDate: now, lastDate: DateTime(now.year + 2));
if (picked != null) setState(() => _deadline = picked);
}
Future<void> _send() async {
final site = _site, token = _token;
if (site == null || token == null) return;
setState(() {
_token = null; // a token works only once
_resetId++;
});
try {
final deadline = _deadline;
await frontmail.send(
templateId: 'tpl_…',
params: {
'name': _name.text,
'email': _email.text,
'service': _service,
'budget': _budget,
'deadline': deadline == null ? null : deadline.toIso8601String().substring(0, 10),
'details': _details.text,
},
options: SendOptions(turnstileToken: token, turnstileKey: site.turnstileKey),
);
setState(() => _info = 'Thanks! We’ve emailed you a confirmation.');
} on FrontmailException catch (e) {
setState(() => _info = e.message);
}
}
@override
Widget build(BuildContext context) {
final site = _site;
return Scaffold(
appBar: AppBar(title: const Text('Request a quote')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
TextField(controller: _name, decoration: const InputDecoration(labelText: 'Name')),
TextField(controller: _email, decoration: const InputDecoration(labelText: 'Email'), keyboardType: TextInputType.emailAddress),
DropdownButtonFormField<String>(
value: _service,
decoration: const InputDecoration(labelText: 'What do you need?'),
items: [for (final s in services) DropdownMenuItem(value: s, child: Text(s))],
onChanged: (v) => setState(() => _service = v ?? services.first),
),
DropdownButtonFormField<String>(
value: _budget,
decoration: const InputDecoration(labelText: 'Budget (optional)'),
items: [for (final b in budgets) DropdownMenuItem(value: b, child: Text(b))],
onChanged: (v) => setState(() => _budget = v),
),
ListTile(
contentPadding: EdgeInsets.zero,
title: Text(_deadline == null ? 'Deadline (optional)' : 'Deadline: ${MaterialLocalizations.of(context).formatMediumDate(_deadline!)}'),
trailing: const Icon(Icons.calendar_today),
onTap: _pickDeadline,
),
TextField(controller: _details, decoration: const InputDecoration(labelText: 'Project details'), maxLines: 5),
const SizedBox(height: 16),
if (site != null)
TurnstileView(
site: site,
resetId: _resetId,
onMessage: (m) => setState(() => _token = m is TurnstileToken ? m.token : null),
),
FilledButton(onPressed: _token == null ? null : _send, child: const Text('Request a quote')),
Text(_info),
],
),
);
}
}
I valori null (nessun budget, nessuna scadenza) sono trattati come vuoti, quindi quelle righe
semplicemente non compaiono nell’email. La scadenza viene inviata come YYYY-MM-DD; nell’email
appare come data formattata.
5. Provalo
Invia una richiesta usando il tuo indirizzo come email del cliente. Ricevi la richiesta e, subito dopo, la conferma. Entrambe compaiono in Cronologia.
Ogni email consuma un credito: una richiesta con la sua conferma ne costa due. Crediti esauriti in una settimana intensa? Le richieste aspettano nella coda dei messaggi in attesa e partono appena ricarichi; lo stato del risultato è
held.
Prossimi passi
- Attiva la Raccolta contatti nelle impostazioni del modello per tenere un elenco di tutti
quelli che hanno chiesto un preventivo (
email,nameecompanysono già mappati). - Invia
SendOptions(locale: Localizations.localeOf(context).toLanguageTag())per usare la variante linguistica del modello nella lingua dell’utente.


