Vai al contenuto

Come creare un modulo di contatto in Flutter che invia email

Un modulo di contatto in Flutter che ti invia ogni messaggio via email: niente backend, nessuna password SMTP nell’app. Funziona su Android, iOS, web e desktop con il pacchetto Dart di Frontmail.

Aggiornato:

Il video è in inglese; i passaggi qui sotto sono il tutorial completo.

Aggiungerai a un’app Flutter una schermata di contatto che invia ogni messaggio alla tua casella di posta. L’app contiene solo una chiave pubblica; Frontmail invia l’email tramite la tua casella, quindi nel bundle dell’app non finiscono né password SMTP né server.

Ti serve: un account Frontmail gratuito, un progetto Flutter 3.24+ e circa un quarto d’ora.

1. Collega la tua casella e crea il modello

  1. Servizi email → Aggiungi servizio: collega Gmail, Outlook, SMTP o un provider transazionale, seleziona Imposta come predefinito, poi Verifica e salva.
  2. Modelli → Dalla galleria → Modulo di contatto → Usa modello. Si aspetta name, email e message (obbligatori) e phone, topic (facoltativi).
  3. Copia l’ID del modello (tpl_…) dalla scheda Snippet.

2. Consenti le app mobile

Le app non inviano l’header Origin come fanno i siti web, quindi l’API le rifiuta finché non le autorizzi: Sicurezza → App mobile → attiva Consenti app mobile → Salva.

Poi apri Impostazioni → Protezione del modello e attiva Richiedi CAPTCHA (Turnstile). Le app mobile usano il widget Turnstile condiviso di Frontmail, quindi non ti servono chiavi Cloudflare tue.

3. Installa il pacchetto

flutter pub add frontmail webview_flutter url_launcher

webview_flutter mostra la verifica Turnstile. Su Android le build di release richiedono anche <uses-permission android:name="android.permission.INTERNET"/> in AndroidManifest.xml.

Copia il widget TurnstileView dalla sezione Turnstile della documentazione dell’SDK Flutter in lib/turnstile_view.dart: sono una sessantina di righe tue, che puoi personalizzare come vuoi.

4. Crea il modulo

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),
        ],
      ),
    );
  }
}

L’SDK ritenta da solo gli errori di rete e i problemi temporanei del server con un’unica chiave di idempotenza, quindi una connessione mobile instabile non produce mai email duplicate.

5. Provalo

Avvia l’app, compila il modulo e tocca Send. L’email arriva nella tua casella e compare in Cronologia nella dashboard.

  • origin_not_allowed – Consenti app mobile è disattivato (o non è stato salvato).
  • captcha_required – manca il token Turnstile; aspetta che la verifica finisca.
  • invalid_template_params – un campo obbligatorio è vuoto oppure l’indirizzo email non è valido.

Se finisci i crediti, il messaggio viene accettato con lo stato held e inviato automaticamente appena ricarichi: con result.status == DeliveryStatus.held puoi avvisare l’utente.

Prossimi passi

  • Invia all’utente una conferma con il modello Risposta automatica: grazie per il messaggio della galleria.
  • Passa SendOptions(locale: …) per inviare l’email nella lingua dell’utente, se il modello ha varianti linguistiche.
Tutti i tutorial