Aller au contenu

Comment créer un formulaire de contact dans Flutter qui envoie des e-mails

Un formulaire de contact Flutter qui vous envoie chaque message par e-mail – sans backend, sans mot de passe SMTP dans l’application. Fonctionne sur Android, iOS, le web et le desktop avec le paquet Dart de Frontmail.

Mis à jour:

La vidéo est en anglais ; le texte ci-dessous est le tutoriel complet.

Vous allez ajouter un écran de contact à une application Flutter qui envoie chaque message dans votre boîte de réception. L’application ne contient qu’une clé publique ; Frontmail envoie l’e-mail via votre propre boîte mail, si bien qu’aucun mot de passe SMTP ni serveur ne se retrouve dans le bundle de l’application.

Il vous faut : un compte Frontmail gratuit, un projet Flutter 3.24+ et environ quinze minutes.

1. Connectez votre boîte mail et créez le modèle

  1. Services d’envoi → Ajouter un service : connectez Gmail, Outlook, SMTP ou un fournisseur transactionnel, cochez Définir par défaut, puis Vérifier et enregistrer.
  2. Modèles → Depuis la galerie → Formulaire de contact → Utiliser ce modèle. Il attend name, email et message (obligatoires) ainsi que phone et topic (facultatifs).
  3. Copiez l’ID du modèle (tpl_…) depuis l’onglet Extraits de code.

2. Autorisez les applications mobiles

Contrairement aux sites web, les applications n’envoient pas d’en-tête Origin : l’API les refuse donc tant que vous ne les avez pas autorisées. Sécurité → Applications mobiles → activez Autoriser les applications mobiles → Enregistrer.

Ouvrez ensuite les Paramètres du modèle → Protection et activez Exiger un CAPTCHA (Turnstile). Les applications mobiles utilisent le widget Turnstile partagé de Frontmail : vous n’avez besoin d’aucune clé Cloudflare.

3. Installez le paquet

flutter pub add frontmail webview_flutter url_launcher

webview_flutter affiche la vérification Turnstile. Sur Android, les builds de release ont aussi besoin de <uses-permission android:name="android.permission.INTERNET"/> dans AndroidManifest.xml.

Copiez le widget TurnstileView depuis la section Turnstile de la documentation du SDK Flutter dans lib/turnstile_view.dart – une soixantaine de lignes qui vous appartiennent et que vous pouvez restyler à votre guise.

4. Créez le formulaire

import 'package:flutter/material.dart';
import 'package:frontmail/frontmail.dart';

import 'turnstile_view.dart';

final frontmail = Frontmail(publicKey: 'pk_…'); // create once, share

class ContactScreen extends StatefulWidget {
  const ContactScreen({super.key});

  @override
  State<ContactScreen> createState() => _ContactScreenState();
}

class _ContactScreenState extends State<ContactScreen> {
  final _name = TextEditingController();
  final _email = TextEditingController();
  final _message = TextEditingController();
  TurnstileSite? _site;
  String? _token;
  int _resetId = 0;
  bool _sending = false;
  String _info = '';

  @override
  void initState() {
    super.initState();
    Turnstile.site(client: frontmail).then((site) => setState(() => _site = site));
  }

  Future<void> _send() async {
    final site = _site, token = _token;
    if (site == null || token == null) return;
    setState(() {
      _sending = true;
      _token = null; // a token works only once
      _resetId++;
    });
    try {
      await frontmail.send(
        templateId: 'tpl_…',
        params: {'name': _name.text, 'email': _email.text, 'message': _message.text},
        options: SendOptions(turnstileToken: token, turnstileKey: site.turnstileKey),
      );
      setState(() => _info = 'Thank you! We’ll get back to you soon.');
    } on FrontmailException catch (e) {
      setState(() => _info = e.message);
    } finally {
      setState(() => _sending = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    final site = _site;
    return Scaffold(
      appBar: AppBar(title: const Text('Contact us')),
      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),
          TextField(controller: _message, decoration: const InputDecoration(labelText: 'Message'), 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 || _sending ? null : _send, child: const Text('Send')),
          Text(_info),
        ],
      ),
    );
  }
}

Le SDK relance de lui-même les requêtes après une erreur réseau ou un incident serveur, avec une seule clé d’idempotence : une connexion mobile instable ne produit jamais d’e-mails en double.

5. Testez

Lancez l’application, remplissez le formulaire et touchez Send. L’e-mail arrive dans votre boîte de réception et apparaît dans l’Historique du tableau de bord.

  • origin_not_allowed – Autoriser les applications mobiles est désactivé (ou non enregistré).
  • captcha_required – le jeton Turnstile manque ; attendez la fin de la vérification.
  • invalid_template_params – un champ obligatoire est vide ou l’adresse e-mail n’est pas valide.

Si vous n’avez plus de crédits, le message est accepté avec le statut held et envoyé automatiquement dès que vous rechargez – result.status == DeliveryStatus.held vous permet d’en informer l’utilisateur.

Étapes suivantes

  • Envoyez une confirmation à l’utilisateur avec le modèle Réponse automatique : merci pour votre message de la galerie.
  • Passez SendOptions(locale: …) pour envoyer l’e-mail dans la langue de l’utilisateur lorsque le modèle a des variantes linguistiques.
Tous les tutoriels