Jak ve Flutteru udělat poptávkový formulář
Poptávkový formulář ve Flutteru s výběrem služby, rozpočtem a termínem, který vám každou poptávku pošle e-mailem a zákazníkovi automaticky potvrdí přijetí – bez backendu.
Aktualizováno:
V tomto návodu postavíte ve Flutteru obrazovku pro poptávku: zákazník vybere službu, rozpočet a termín, popíše projekt a klepne na Send. Vy dostanete úhledně naformátovaný e-mail, zákazník automatické potvrzení.
Budete potřebovat: bezplatný účet Frontmail, projekt ve Flutteru 3.24+ a zhruba dvacet minut.
1. Připojte schránku a vytvořte šablony
- E-mailové služby → Přidat službu: připojte svou schránku nebo poskytovatele, zaškrtněte Nastavit jako výchozí a klikněte na Ověřit a uložit.
- Šablony → Z galerie → Poptávka (kategorie Byznys) → Použít šablonu.
- Přidejte potvrzení: Z galerie → Automatická odpověď: díky za zprávu → Použít
šablonu, na její záložce Parametry nastavte výchozí hodnoty
company_namearesponse_timea pak ji v šabloně poptávky vyberte v Nastavení → Doručování → Šablona automatické odpovědi.
Šablona poptávky očekává name, email, service a details (povinné) a phone, company,
budget, deadline (nepovinné). service a budget přijímají jen hodnoty uvedené na záložce
Parametry – upravte seznamy podle toho, co nabízíte, a v aplikaci použijte stejné řetězce.
Příklad kódu používá hodnoty anglické šablony; česká šablona má Web, E-shop, Mobilní aplikace,
Konzultace, Jiné a Do 100 tis. Kč, 100–500 tis. Kč, 500 tis. – 1 mil. Kč, Nad 1 mil. Kč.
Aplikace musí posílat přesně hodnoty ze záložky Parametry.
2. Povolte mobilní aplikace a zapněte CAPTCHA
- Zabezpečení → Mobilní aplikace → Povolit mobilní aplikace → Uložit. Bez toho se
požadavky bez webového
Originodmítají s chybouorigin_not_allowed. - Šablona poptávky → Nastavení → Ochrana → Vyžadovat CAPTCHA (Turnstile). Aplikace používají sdílený widget Frontmailu – účet u Cloudflaru nepotřebujete.
3. Instalace
flutter pub add frontmail webview_flutter url_launcher
Z části o Turnstile v dokumentaci Flutter SDK si zkopírujte TurnstileView do
lib/turnstile_view.dart.
4. Postavte formulář
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),
],
),
);
}
}
Hodnoty null (bez rozpočtu, bez termínu) se berou jako prázdné, takže se tyto řádky v e-mailu
prostě neobjeví. Termín se posílá jako YYYY-MM-DD; e-mail ho zobrazí jako naformátované datum.
5. Vyzkoušejte to
Odešlete poptávku se svou adresou jako e-mailem zákazníka. Přijde vám poptávka a hned po ní potvrzení. Obojí najdete v sekci Historie.
Každý e-mail stojí jeden kredit – poptávka i s potvrzením tedy dva. Došly vám kredity v rušném týdnu? Poptávky počkají ve frontě pozdržených zpráv a odejdou po dobití; stav výsledku je
held.
Další kroky
- Zapněte v nastavení šablony Sběr kontaktů a budete mít seznam všech, kdo poptávali
(
email,nameacompanyjsou už namapované). - Pošlete
SendOptions(locale: Localizations.localeOf(context).toLanguageTag()), aby se použila jazyková verze šablony v jazyce uživatele.


