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:
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
- 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.
- Szablony → Z galerii → Formularz kontaktowy → Użyj szablonu. Szablon oczekuje
parametrów
name,emailimessage(wymagane) orazphone,topic(opcjonalne). - 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
heldi wysłana automatycznie po doładowaniu – dziękiresult.status == DeliveryStatus.heldmoż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.


