Przejdź do treści

Jak zrobić formularz kontaktowy we Flutterze, który wysyła e-maile

Formularz kontaktowy we Flutterze, który wysyła Ci każdą wiadomość e-mailem – bez backendu i bez hasła SMTP w aplikacji. Działa na Androidzie, iOS, w przeglądarce i na desktopie dzięki pakietowi Frontmail dla Darta.

Zaktualizowano:

Film jest po angielsku; poniżej znajdziesz pełny samouczek krok po kroku.

Dodasz do aplikacji we Flutterze ekran kontaktowy, który każdą wiadomość wysyła do Twojej skrzynki. Aplikacja zna tylko klucz publiczny; e-mail wysyła Frontmail przez Twoją skrzynkę, więc w paczce aplikacji nie ląduje żadne hasło SMTP ani adres serwera.

Potrzebujesz: darmowego konta Frontmail, projektu we Flutterze 3.24+ i około piętnastu minut.

1. Podłącz skrzynkę i utwórz szablon

  1. Usługi e-mail → Dodaj usługę: podłącz Gmail, Outlook, SMTP albo dostawcę e-maili transakcyjnych, zaznacz Ustaw jako domyślną, Zweryfikuj i zapisz.
  2. Szablony → Z galerii → Formularz kontaktowy → Użyj szablonu. Szablon oczekuje parametrów name, email i message (wymagane) oraz phone, topic (opcjonalne).
  3. Skopiuj ID szablonu (tpl_…) z zakładki Kod.

2. Zezwól na aplikacje mobilne

Aplikacje nie wysyłają nagłówka Origin tak jak strony internetowe, więc API je odrzuca, dopóki na to nie zezwolisz: Bezpieczeństwo → Aplikacje mobilne → włącz Zezwól na aplikacje mobilne → Zapisz.

Potem otwórz w szablonie Ustawienia → Ochrona i włącz Wymagaj CAPTCHA (Turnstile). Aplikacje mobilne korzystają ze wspólnego widżetu Turnstile Frontmail, więc nie potrzebujesz własnych kluczy Cloudflare.

3. Zainstaluj pakiet

flutter pub add frontmail webview_flutter url_launcher

webview_flutter wyświetla weryfikację Turnstile. Na Androidzie buildy release potrzebują też <uses-permission android:name="android.permission.INTERNET"/> w AndroidManifest.xml.

Skopiuj widżet TurnstileView z sekcji o Turnstile w dokumentacji Flutter SDK do pliku lib/turnstile_view.dart – to około sześćdziesięciu linii, które są Twoje i które możesz dowolnie ostylować.

4. Zbuduj formularz

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

SDK samo ponawia żądania po błędach sieci i chwilowych problemach serwera z jednym kluczem idempotencji, więc niestabilne połączenie komórkowe nigdy nie wygeneruje zduplikowanych e-maili.

5. Przetestuj

Uruchom aplikację, wypełnij formularz i stuknij Send. E-mail dotrze do Twojej skrzynki i pojawi się w panelu w sekcji Historia.

  • origin_not_allowed – opcja Zezwól na aplikacje mobilne jest wyłączona (albo niezapisana).
  • captcha_required – brakuje tokenu Turnstile; poczekaj, aż weryfikacja się zakończy.
  • invalid_template_params – wymagane pole jest puste albo adres e-mail jest nieprawidłowy.

Jeśli skończą Ci się kredyty, wiadomość zostanie przyjęta ze statusem held i wysłana automatycznie po doładowaniu – dzięki result.status == DeliveryStatus.held możesz o tym poinformować użytkownika.

Co dalej

  • Wyślij użytkownikowi potwierdzenie szablonem Automatyczna odpowiedź: dziękujemy za wiadomość z galerii.
  • Przekaż SendOptions(locale: …), żeby wysłać e-mail w języku użytkownika, jeśli szablon ma wersje językowe.
Wszystkie samouczki