Comment créer un formulaire de demande de devis dans Flutter
Un formulaire de demande de devis Flutter avec choix de la prestation, budget et échéance, qui vous envoie chaque demande par e-mail et envoie au client une confirmation automatique – sans backend.
Mis à jour:
Ce tutoriel crée un écran de demande de devis dans Flutter : le client choisit une prestation, un budget et une échéance, décrit son projet et touche Send. Vous recevez un e-mail proprement mis en forme, le client reçoit une confirmation automatique.
Il vous faut : un compte Frontmail gratuit, un projet Flutter 3.24+ et une vingtaine de minutes.
1. Connectez votre boîte mail et créez les modèles
- Services d’envoi → Ajouter un service : connectez votre boîte mail ou votre fournisseur, cochez Définir par défaut, puis Vérifier et enregistrer.
- Modèles → Depuis la galerie → Demande de devis (catégorie Entreprise) → Utiliser ce modèle.
- Ajoutez la confirmation : Depuis la galerie → Réponse automatique : merci pour votre
message → Utiliser ce modèle, définissez les valeurs par défaut de
company_nameetresponse_timedans ses Paramètres, puis sélectionnez-le dans le modèle de devis sous Paramètres → Envoi → Modèle de réponse automatique.
Le modèle de devis attend name, email, service et details (obligatoires) ainsi que
phone, company, budget et deadline (facultatifs). service et budget n’acceptent que
les valeurs listées dans Paramètres. Le code ci-dessous utilise les valeurs du modèle anglais ;
si vous créez le modèle en français, ce sont Site web, Boutique en ligne, Application mobile,
Conseil, Autre et Moins de 5 000 €, 5 000 – 20 000 €, 20 000 – 50 000 €, Plus de 50 000 €.
Adaptez les listes à votre offre et reprenez exactement les mêmes chaînes dans l’application.
2. Autorisez les applications mobiles et activez le CAPTCHA
- Sécurité → Applications mobiles → Autoriser les applications mobiles →
Enregistrer. Sans cela, les requêtes sans
Originde site web sont refusées avecorigin_not_allowed. - Modèle de devis → Paramètres → Protection → Exiger un CAPTCHA (Turnstile). Les applications utilisent le widget partagé de Frontmail – aucun compte Cloudflare n’est nécessaire.
3. Installez
flutter pub add frontmail webview_flutter url_launcher
Copiez TurnstileView depuis la section Turnstile de la documentation du SDK Flutter dans
lib/turnstile_view.dart.
4. Créez le formulaire
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),
],
),
);
}
}
Les valeurs null (pas de budget, pas d’échéance) sont traitées comme vides : ces lignes
n’apparaissent tout simplement pas dans l’e-mail. L’échéance est envoyée au format YYYY-MM-DD ;
l’e-mail l’affiche comme une date mise en forme.
5. Testez
Envoyez une demande avec votre propre adresse comme e-mail du client. Vous recevez la demande puis, juste après, la confirmation. Les deux figurent dans l’Historique.
Chaque e-mail consomme un crédit – une demande et sa confirmation en coûtent deux. Plus de crédits pendant une semaine chargée ? Les demandes patientent dans la file d’attente des e-mails retenus et partent dès que vous rechargez ; le statut du résultat est
held.
Étapes suivantes
- Activez la Collecte des contacts dans les paramètres du modèle pour garder la liste de toutes
les personnes qui ont demandé un devis (
email,nameetcompanysont déjà associés). - Envoyez
SendOptions(locale: Localizations.localeOf(context).toLanguageTag())pour utiliser la variante linguistique du modèle correspondant à la langue de l’utilisateur.


