Flutter a Dart
Balík frontmail umožní aplikácii vo Flutteri odosielať e-maily cez vaše šablóny s verejným
kľúčom – bez vlastného backendu. Je napísaný čisto v Darte (jedinou závislosťou je package:http),
takže rovnaký kód beží na Androide, iOS, macOS, Windows, Linuxe, na webe aj v programoch v Darte.
Požiadavky: Dart 3.5+ (Flutter 3.24+).
Inštalácia
Section titled “Inštalácia”flutter pub add frontmailBalík: pub.dev/packages/frontmail. Zdrojový kód a vydania: github.com/frontmail-dev/frontmail-flutter.
Release buildy pre Android potrebujú oprávnenie INTERNET v
android/app/src/main/AndroidManifest.xml (Flutter ho sám pridáva len do debug buildov):
<uses-permission android:name="android.permission.INTERNET"/>Na macOS pridajte entitlement com.apple.security.network.client do oboch súborov
macos/Runner/DebugProfile.entitlements aj macos/Runner/Release.entitlements.
Povoľte mobilné aplikácie
Section titled “Povoľte mobilné aplikácie”Natívne aplikácie neposielajú hlavičku Origin ani Referer, takže požiadavky z aplikácie (bez
ohľadu na to, či máte nastavené povolené weby) skončia chybou
403 origin_not_allowed, kým v dashboarde nezapnete
Zabezpečenie → Mobilné aplikácie → Povoliť mobilné aplikácie a nekliknete na Uložiť.
Pozrite Nastavenia zabezpečenia.
Požiadavky z webov sa naďalej kontrolujú podľa zoznamu, takže prepínač ochranu vašich webových formulárov neoslabí. Prepínač je potrebný aj pri prázdnom zozname.
Flutter web beží v prehliadači, ktorý hlavičku Origin posiela vždy: namiesto prepínača
pridajte origin svojej webovej aplikácie medzi povolené weby.
Odosielanie
Section titled “Odosielanie”Klienta vytvorte raz a zdieľajte ho (top-level final, provider, vaše DI) a volajte send:
import 'package:frontmail/frontmail.dart';
final frontmail = Frontmail(publicKey: 'pk_4f2a…');
final result = await frontmail.send( templateId: 'tpl_contact', params: {'email': 'jan@example.com', 'message': 'Dobrý deň!', 'qty': 2, 'items': [{'sku': 'A-1'}]}, serviceId: 'svc_01J9…', // voliteľné – null použije predvolenú službu šablóny);if (result.status == DeliveryStatus.held) { // Prijaté – doručí sa o chvíľu (organizácia čaká na kredity).}send vracia SendResult s messageId, status (queued alebo held) a statusToken. Keď
odovzdáte súkromný kľúč, konštruktor vyhodí chybu (pozrite Zabezpečenie).
Voľby konštruktora:
final frontmail = Frontmail( publicKey: 'pk_4f2a…', apiUrl: Uri.parse('https://api.frontmail.dev'), // predvolené retry: const RetryPolicy(maxRetries: 3), // predvolené; RetryPolicy.disabled = bez opakovania timeout: const Duration(seconds: 15), // na jeden pokus (predvolené) httpClient: myClient, // ľubovoľný Client z package:http, napr. cupertino_http alebo cronet_http);close() zatvorí HTTP klienta, ktorého si SDK vytvorilo samo; klient, ktorého ste odovzdali vy,
zostane otvorený. Každá požiadavka nesie hlavičku X-Frontmail-Client: frontmail-flutter/<verzia>.
Parametre
Section titled “Parametre”Parametre sú Map<String, Object?> s hodnotami JSON: String, num, bool, null, zoznamy
a mapy. Hodnoty DateTime sa pošlú ako reťazce ISO-8601 v UTC a objekty s metódou toJson()
(napríklad z json_serializable alebo freezed) sa zakódujú cez ňu:
await frontmail.send(templateId: 'tpl_contact', params: contact.toJson());Hodnoty, ktoré sa do JSON previesť nedajú (double.nan, objekt bez toJson()), skončia chybou
invalid_template_params ešte pred odoslaním.
Prílohy a ďalšie voľby
Section titled “Prílohy a ďalšie voľby”SendOptions nesie token Turnstile, prílohy, jazyk šablóny, prípadne vlastný idempotency key
a spúšťač prerušenia:
await frontmail.send( templateId: 'tpl_order', params: {'order': '1042'}, options: SendOptions( turnstileToken: token, attachments: [ Attachment.bytes(pdfBytes, filename: 'faktura.pdf', contentType: 'application/pdf'), const Attachment.upload('upl_…'), // nahrané skôr cez POST /v1/uploads ], ),);SendOptions(locale: …) pošle e-mail v niektorom jazyku šablóny
(značka BCP 47). Použite Localizations.localeOf(context).toLanguageTag() (napr. de-AT) –
Locale.toString() používa _ a odmietne sa ako invalid_body. Pri neznámom jazyku sa použije
rovnaký základný jazyk a potom predvolený jazyk šablóny; result.locale je použitý jazyk:
final result = await frontmail.send( templateId: 'tpl_contact', params: {'name': 'Jan'}, options: SendOptions(locale: Localizations.localeOf(context).toLanguageTag()),);print(result.locale ?? 'default'); // "de"Future v Darte sa zrušiť nedá, preto send aj status prijímajú abortTrigger: keď sa táto
future dokončí – napríklad v dispose() – požiadavka alebo čakanie pred ďalším pokusom skončí
s kódom aborted:
final abort = Completer<void>();frontmail.send(templateId: 'tpl_contact', options: SendOptions(abortTrigger: abort.future));// neskôrabort.complete();Stav doručenia
Section titled “Stav doručenia”So statusToken z výsledku send si aplikácia prečíta stav danej správy. SDK token posiela
v hlavičke X-Frontmail-Status-Token, nikdy v URL:
final status = await frontmail.status(messageId: result.messageId, statusToken: result.statusToken);print(status.status); // queued, sending, sent, delivered, bounced, … (neznáme hodnoty sa zachovajú)print(status.events.map((e) => e.type)); // (accepted, sent, …)Turnstile
Section titled “Turnstile”Ak šablóna vyžaduje Turnstile, zobrazte widget Cloudflare vo web view
z webview_flutter a token odovzdajte do send.
Site key nepotrebujete: v predvolenom stave sa použije zdieľaný mobilný kľúč Frontmailu.
Turnstile.site(client:) ho načíta z GET /v1/public-config
(s cache v pamäti, takže len raz) a widget beží ako vložená HTML stránka s adresou
https://mobile.frontmail.dev – z tej adresy sa nič nesťahuje.
SDK dodáva jednotlivé diely, samotný widget patrí do vašej aplikácie (vďaka tomu SDK zostáva v čistom Darte):
| API | Čo robí |
|---|---|
Turnstile.site(client:) |
site key + adresa stránky + turnstileKey, s ktorým sa tokeny posielajú |
Turnstile.html(siteKey:, theme:, size:, action:, language:) |
stránka s widgetom; správy posiela do JavaScript kanála FrontmailTurnstile |
Turnstile.parseMessage(message) |
overí správu → TurnstileToken, TurnstileExpired, TurnstileError |
Turnstile.navigationAction(url:, baseUrl:, isTopFrame:) |
stráženie navigácie: TurnstileAllow, TurnstileCancel, TurnstileOpenExternally |
Turnstile.resetScript |
JavaScript, ktorý widget resetuje a vyžiada nový token |
Kompletný widget pre Android, iOS a macOS – pridajte webview_flutter a url_launcher
(flutter pub add webview_flutter url_launcher) a skopírujte si ho do aplikácie:
import 'package:flutter/material.dart';import 'package:frontmail/frontmail.dart';import 'package:url_launcher/url_launcher.dart';import 'package:webview_flutter/webview_flutter.dart';
class TurnstileView extends StatefulWidget { const TurnstileView({ super.key, required this.site, required this.onMessage, this.theme = TurnstileTheme.auto, this.size = TurnstileSize.flexible, this.resetId = 0, });
final TurnstileSite site; final ValueChanged<TurnstileMessage> onMessage; final TurnstileTheme theme; final TurnstileSize size;
/// Zmenou hodnoty widget resetujete a získate nový token (tokeny sa dajú použiť len raz). final int resetId;
@override State<TurnstileView> createState() => _TurnstileViewState();}
class _TurnstileViewState extends State<TurnstileView> { late final WebViewController _controller;
@override void initState() { super.initState(); _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setBackgroundColor(Colors.transparent) ..addJavaScriptChannel( Turnstile.javaScriptChannelName, onMessageReceived: (message) { final parsed = Turnstile.parseMessage(message.message); if (parsed != null && mounted) widget.onMessage(parsed); }, ) // Hlavný rámec zostane na widgete, odkazy z neho sa otvoria v systémovom prehliadači. ..setNavigationDelegate(NavigationDelegate( onNavigationRequest: (request) { final action = Turnstile.navigationAction( url: request.url, baseUrl: widget.site.baseUrl, isTopFrame: request.isMainFrame, ); switch (action) { case TurnstileAllow(): return NavigationDecision.navigate; case TurnstileCancel(): return NavigationDecision.prevent; case TurnstileOpenExternally(:final url): launchUrl(url, mode: LaunchMode.externalApplication); return NavigationDecision.prevent; } }, )) ..loadHtmlString( Turnstile.html(siteKey: widget.site.siteKey, theme: widget.theme, size: widget.size), baseUrl: widget.site.baseUrl.toString(), ); }
@override void didUpdateWidget(TurnstileView oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.resetId != widget.resetId) _controller.runJavaScript(Turnstile.resetScript); }
@override Widget build(BuildContext context) => SizedBox( width: widget.size.recommendedWidth, height: widget.size.recommendedHeight, child: WebViewWidget(controller: _controller), );}Použitie vo formulári: počkať na token, odoslať a widget resetovať:
class ContactForm extends StatefulWidget { const ContactForm({super.key, required this.frontmail});
final Frontmail frontmail;
@override State<ContactForm> createState() => _ContactFormState();}
class _ContactFormState extends State<ContactForm> { final _email = TextEditingController(); final _message = TextEditingController(); TurnstileSite? _site; String? _token; int _resetId = 0; String _info = '';
@override void initState() { super.initState(); Turnstile.site(client: widget.frontmail).then( (site) => setState(() => _site = site), onError: (Object e) => setState(() => _info = '$e'), ); }
@override void dispose() { _email.dispose(); _message.dispose(); super.dispose(); }
Future<void> _submit() async { final site = _site, token = _token; if (site == null || token == null) return; // Token sa dá použiť len raz – nový si vyžiadajte bez ohľadu na výsledok. setState(() { _token = null; _resetId++; }); try { final result = await widget.frontmail.send( templateId: 'tpl_contact', params: {'email': _email.text, 'message': _message.text}, options: SendOptions(turnstileToken: token, turnstileKey: site.turnstileKey), ); setState(() => _info = result.status == DeliveryStatus.held ? 'Prijaté – doručíme o chvíľu.' : 'Ďakujeme!'); } on FrontmailException catch (e) { setState(() => _info = e.message); } }
@override Widget build(BuildContext context) { final site = _site; return Column(children: [ TextField(controller: _email, decoration: const InputDecoration(labelText: 'E-mail')), TextField(controller: _message, decoration: const InputDecoration(labelText: 'Správa')), if (site != null) TurnstileView( site: site, resetId: _resetId, onMessage: (message) => setState(() { switch (message) { case TurnstileToken(:final token): _token = token; case TurnstileExpired(): _token = null; case TurnstileError(:final code): _info = 'Chyba Turnstile ${code ?? ''}'; } }), ), FilledButton(onPressed: _token == null ? null : _submit, child: const Text('Odoslať')), Text(_info), ]); }}Web view je uzamknuté na widget: načíta len vloženú stránku (baseUrl) a výzvu Cloudflaru, odkazy
vo widgete sa otvoria v systémovom prehliadači a parseMessage prijme len správy presne v tej
podobe, akú posiela stránka widgetu.
Požiadavky z aplikácie nemajú hlavičku Origin, takže ich tokeny Frontmail overí svojím zdieľaným
mobilným tajným kľúčom. Vlastné kľúče Turnstile na to nepotrebujete – so zapnutým Povoliť mobilné
aplikácie môžete CAPTCHA pri šablónach aplikácie zapnúť, aj keď organizácia kľúče zatiaľ nemá.
webview_flutter nemá implementáciu pre web. Na Flutter webe požiadavky hlavičku Origin majú,
takže Turnstile potrebuje vlastné kľúče: widget vykreslite priamo
v stránke (napríklad cez HtmlElementView) a jeho token pošlite s turnstileKey: TurnstileKey.org.
Vlastný site key
Section titled “Vlastný site key”Ak chcete namiesto toho použiť vlastný widget Turnstile, vytvorte si site sami:
final site = TurnstileSite.custom(siteKey: 'YOUR_TURNSTILE_SITE_KEY', baseUrl: Uri.parse('https://example.com'));Jeho turnstileKey je TurnstileKey.org, takže
SendOptions(turnstileToken: token, turnstileKey: site.turnstileKey) pošle turnstile_key: "org"
a Frontmail token overí secret key zo Zabezpečenie → Ochrana pred botmi (Turnstile).
Token platí približne 5 minút a dá sa použiť len raz: po každom odoslaní, úspešnom aj neúspešnom, widget resetujte.
Opakovanie a idempotencia
Section titled “Opakovanie a idempotencia”Sieťové chyby, vypršanie časového limitu, 5xx a 429 sa automaticky opakujú (v predvolenom stave
trikrát, s exponenciálnym čakaním a náhodným rozptylom). SDK rešpektuje hlavičku Retry-After; ak
žiada viac než 60 sekúnd, chybu vyhodí hneď. Ostatné odpovede 4xx sa nikdy neopakujú. Všetky
pokusy jedného odoslania zdieľajú rovnaký idempotency key (náhodné UUID, ak neodovzdáte vlastný
idempotencyKey), takže opakovanie e-mail nikdy nepošle dvakrát. Pozrite
Opakovanie a idempotencia.
Všetko vyhadzuje FrontmailException s vlastnosťami code, message, status (null pri
chybách na strane klienta), docsUrl, details a retryAfter. Klientske kódy sú
network_error, timeout, aborted, invalid_response a private_key_in_browser (konštanty
vo FrontmailErrorCode); kontroly isAuth, isValidation, isRateLimit,
isInsufficientCredits, isNetwork a isAborted ich zoskupujú:
try { await frontmail.send(templateId: 'tpl_contact', params: params);} on FrontmailException catch (e) { if (e.isRateLimit) { show('Príliš veľa správ – skúste to znova o ${e.retryAfter?.inSeconds ?? 60} s.'); } else if (e.code == FrontmailErrorCode.originNotAllowed) { assert(false, 'V dashboarde zapnite Zabezpečenie → Mobilné aplikácie → Povoliť mobilné aplikácie.'); } else { show(e.message); }}Ako reagovať na jednotlivé kódy, opisuje Spracovanie chýb.
Zabezpečenie
Section titled “Zabezpečenie”- Do aplikácie patrí len verejný kľúč (
pk_…). Čokoľvek v balíku aplikácie sa dá vytiahnuť, takže súkromný kľúč v nej je ako zverejnený. Kľúče začínajúcesk_SDK odmietne s chybouprivate_key_in_browser. Pozrite Verejný a súkromný kľúč. - So zapnutým Povoliť mobilné aplikácie prijmeme požiadavky bez hlavičky
Originodkiaľkoľvek – poslať ich môže akýkoľvek skript, ktorý pozná váš verejný kľúč. Spoľahnite sa na limit na IP adresu, Turnstile pri všetkých šablónach, ktoré aplikácia používa, a na zoznam blokovaných hodnôt. - Šablóna, ktorá berie príjemcu z parametrov (napríklad potvrdenie na adresu, ktorú používateľ zadal), je uzamknutá: potrebuje Turnstile, pevného odosielateľa, krátke parametre bez HTML a odkazy len na vaše domény. Pozrite Povolené odkazy a zámok obsahu.
Súkromie
Section titled “Súkromie”SDK používateľov nesleduje a samo žiadne údaje nezbiera. To, čo posielajú vaše šablóny (napríklad e-mailová adresa), sú údaje, ktoré zbiera vaša aplikácia: uveďte ich v údajoch o súkromí v App Store a vo formulári Bezpečnosť údajov v Google Play.