iOS a platformy Apple (Swift)
Swift balík Frontmail umožní aplikácii pre iOS (aj pre ostatné platformy Apple) odosielať e-maily
cez vaše šablóny s verejným kľúčom – bez vlastného backendu. Nemá žiadne závislosti, používa
async/await a prekladá sa v jazykovom režime Swift 6.
Požiadavky: Swift 5.9+ (Xcode 15+); iOS 15, macOS 12, tvOS 15, watchOS 8, visionOS 1.
Inštalácia
Section titled “Inštalácia”Xcode: File → Add Package Dependencies… → zadajte
https://github.com/frontmail-dev/frontmail-swift → knižnicu Frontmail pridajte do targetu
aplikácie.
Package.swift:
dependencies: [ .package(url: "https://github.com/frontmail-dev/frontmail-swift", from: "0.1.0"),],targets: [ .target(name: "MyApp", dependencies: [.product(name: "Frontmail", package: "frontmail-swift")]),]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.
Odosielanie
Section titled “Odosielanie”Klienta vytvorte raz (je nemenný a dá sa zdieľať medzi vláknami) a volajte send:
import Frontmail
let frontmail = try Frontmail(publicKey: "pk_4f2a…")
let result = try 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é – nil použije predvolenú službu šablóny)if result.status == .held { // Prijaté – doručí sa o chvíľu (organizácia čaká na kredity).}send vracia SendResult s vlastnosťami messageID, status (.queued alebo .held)
a statusToken. Keď odovzdáte súkromný kľúč, inicializátor vyhodí chybu (pozrite
Zabezpečenie).
Voľby inicializátora:
let frontmail = try Frontmail( publicKey: "pk_4f2a…", apiURL: URL(string: "https://api.frontmail.dev")!, // predvolené retry: RetryPolicy(maxRetries: 3, baseDelay: 0.3, maxDelay: 10), // predvolené; .disabled = bez opakovania timeout: 15, // sekundy na jeden pokus (predvolené) session: .shared // URLSession (predvolené))Každá požiadavka nesie hlavičku X-Frontmail-Client: frontmail-swift/<verzia>.
Parametre
Section titled “Parametre”Parametre sú [String: FrontmailValue] – hodnota JSON, ktorú zapíšete bežnými literálmi
(reťazce, čísla, booleany, nil, polia a slovníky). Hodnoty z premenných zabaľte do príslušného
prípadu: ["email": .string(email)].
Poslúži aj ľubovoľná hodnota Encodable, z ktorej vznikne objekt JSON. Názvy vlastností sa pošlú
bez zmeny (premenujete ich cez CodingKeys), dátumy ako reťazce ISO-8601:
struct Contact: Encodable { let email: String let message: String}
try await frontmail.send(templateID: "tpl_contact", params: Contact(email: email, message: message))Prílohy a ďalšie voľby
Section titled “Prílohy a ďalšie voľby”SendOptions nesie token Turnstile, prílohy, prípadne vlastný idempotency key a jazyk šablóny:
try await frontmail.send( templateID: "tpl_order", params: ["order": "1042"], options: SendOptions( turnstileToken: token, attachments: [ .data(pdfData, filename: "faktura.pdf", contentType: "application/pdf"), .upload(id: "upl_…"), // nahrané skôr cez POST /v1/uploads ] ))SendOptions(locale: …) pošle e-mail v niektorom jazyku šablóny
(značka BCP 47). Pre jazyk zariadenia použite Locale.preferredLanguages.first (napr. de-AT) –
Locale.current.identifier obsahuje _ 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:
let result = try await frontmail.send( templateID: "tpl_contact", params: ["name": "Jan"], options: SendOptions(locale: Locale.preferredLanguages.first))print(result.locale ?? "default") // "de"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:
let status = try await frontmail.status(messageID: result.messageID, statusToken: result.statusToken)print(status.status) // .queued, .sending, .sent, .delivered, .bounced, … alebo .unknown("…")print(status.events.map(\.type)) // ["accepted", "sent", …]Turnstile
Section titled “Turnstile”Ak šablóna vyžaduje Turnstile, zobrazte widget Cloudflare vo
WKWebView a token odovzdajte do send. Site key nepotrebujete: v predvolenom stave sa použije
zdieľaný mobilný kľúč Frontmailu. Turnstile.widget(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é WebView patrí do vašej aplikácie:
| API | Čo robí |
|---|---|
Turnstile.widget(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 script message handlera frontmailTurnstile |
Turnstile.parseMessage(_:) |
overí správu → .token(String), .expired, .error(code:) |
Turnstile.navigationAction(url:baseURL:isTopFrame:) |
stráženie navigácie: .allow, .cancel, .openExternally(URL) |
Turnstile.isTrustedMessageOrigin(_:baseURL:) |
prijme správy len zo stránky widgetu |
Turnstile.resetScript |
JavaScript, ktorý widget resetuje a vyžiada nový token |
Kompletné view pre SwiftUI – skopírujte si ho do aplikácie:
import SwiftUIimport WebKitimport Frontmail
struct TurnstileView: UIViewRepresentable { let widget: Turnstile.Widget var theme: Turnstile.Theme = .auto var size: Turnstile.Size = .normal /// Zmenou hodnoty widget resetujete a získate nový token (tokeny sa dajú použiť len raz). var resetID = 0 let onMessage: (Turnstile.Message) -> Void
func makeCoordinator() -> Coordinator { Coordinator(self) }
func makeUIView(context: Context) -> WKWebView { let config = WKWebViewConfiguration() config.websiteDataStore = .nonPersistent() config.preferences.javaScriptCanOpenWindowsAutomatically = false config.userContentController.add(context.coordinator, name: Turnstile.messageHandlerName)
let webView = WKWebView(frame: .zero, configuration: config) webView.navigationDelegate = context.coordinator webView.uiDelegate = context.coordinator webView.isOpaque = false webView.backgroundColor = .clear webView.scrollView.isScrollEnabled = false context.coordinator.resetID = resetID webView.loadHTMLString(Turnstile.html(siteKey: widget.siteKey, theme: theme, size: size), baseURL: widget.baseURL) return webView }
func updateUIView(_ webView: WKWebView, context: Context) { context.coordinator.parent = self if context.coordinator.resetID != resetID { context.coordinator.resetID = resetID webView.evaluateJavaScript(Turnstile.resetScript) } }
static func dismantleUIView(_ webView: WKWebView, coordinator: Coordinator) { webView.configuration.userContentController.removeScriptMessageHandler(forName: Turnstile.messageHandlerName) }
@MainActor final class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate, WKUIDelegate { var parent: TurnstileView var resetID = 0
init(_ parent: TurnstileView) { self.parent = parent }
// S aplikáciou smie hovoriť len vložená stránka (hlavný rámec s adresou baseURL). func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) { guard message.frameInfo.isMainFrame, Turnstile.isTrustedMessageOrigin(message.frameInfo.request.url, baseURL: parent.widget.baseURL), let parsed = Turnstile.parseMessage(message.body) else { return } parent.onMessage(parsed) }
// Hlavný rámec zostane na widgete, odkazy z neho sa otvoria v systémovom prehliadači. func webView( _ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void ) { switch Turnstile.navigationAction( url: navigationAction.request.url, baseURL: parent.widget.baseURL, isTopFrame: navigationAction.targetFrame?.isMainFrame ) { case .allow: decisionHandler(.allow) case .cancel: decisionHandler(.cancel) case let .openExternally(url): decisionHandler(.cancel) UIApplication.shared.open(url) } }
// window.open / target="_blank": druhé WebView nikdy nevytvárať. func webView( _ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures ) -> WKWebView? { if case let .openExternally(url) = Turnstile.navigationAction( url: navigationAction.request.url, baseURL: parent.widget.baseURL, isTopFrame: true ) { UIApplication.shared.open(url) } return nil } }}Použitie vo formulári: získať token, odoslať a widget resetovať:
struct ContactView: View { let frontmail: Frontmail @State private var widget: Turnstile.Widget? @State private var token: String? @State private var resetID = 0 @State private var email = "" @State private var message = "" @State private var info = ""
var body: some View { Form { TextField("E-mail", text: $email) TextField("Správa", text: $message) if let widget { TurnstileView(widget: widget, size: .flexible, resetID: resetID) { event in switch event { case let .token(t): token = t case .expired: token = nil case let .error(code): info = "Chyba Turnstile \(code ?? "")" } } .frame(height: Turnstile.Size.flexible.recommendedHeight) } Button("Odoslať") { Task { await submit() } }.disabled(token == nil) Text(info) } .task { do { widget = try await Turnstile.widget(client: frontmail) } catch { info = "\(error)" } } }
private func submit() async { guard let widget, let token else { return } // Token sa dá použiť len raz – nový si vyžiadajte bez ohľadu na výsledok. defer { self.token = nil; resetID += 1 } do { let result = try await frontmail.send( templateID: "tpl_contact", params: ["email": .string(email), "message": .string(message)], options: SendOptions(turnstileToken: token, turnstileKey: widget.turnstileKey) ) info = result.status == .held ? "Prijaté – doručíme o chvíľu." : "Ďakujeme!" } catch let error as FrontmailError { info = error.message } catch { info = "\(error)" } }}WebView 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, druhé okno sa otvoriť nedá a do vášho kódu sa
dostanú len správy zo stránky widgetu. Rovnaký Coordinator funguje aj v UIKite.
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á.
Vlastný site key
Section titled “Vlastný site key”Ak chcete namiesto toho použiť vlastný widget Turnstile, vytvorte si widget sami:
let widget = Turnstile.Widget.custom(siteKey: "YOUR_TURNSTILE_SITE_KEY", baseURL: URL(string: "https://example.com")!)Jeho turnstileKey je .org, takže SendOptions(turnstileToken: token, turnstileKey: widget.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. Zrušenie volajúceho Task
požiadavku okamžite ukončí s kódom aborted. Pozrite
Opakovanie a idempotencia.
Všetko vyhadzuje FrontmailError s vlastnosťami code, message, status (nil 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; kontroly isAuth, isValidation,
isRateLimit, isInsufficientCredits, isNetwork a isCancelled ich zoskupujú:
do { try await frontmail.send(templateID: "tpl_contact", params: params)} catch let error as FrontmailError where error.isRateLimit { show("Príliš veľa správ – skúste to znova o \(Int(error.retryAfter ?? 60)) s.")} catch let error as FrontmailError where error.code == .originNotAllowed { assertionFailure("V dashboarde zapnite Zabezpečenie → Mobilné aplikácie → Povoliť mobilné aplikácie.")} catch let error as FrontmailError { show(error.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, samo žiadne údaje nezbiera a nepoužíva API, pri ktorých Apple vyžaduje
uviesť dôvod – to isté uvádza jeho PrivacyInfo.xcprivacy. To, čo posielajú vaše šablóny
(napríklad e-mailová adresa), sú údaje, ktoré zbiera vaša aplikácia: uveďte ich v jej privacy
manifeste a v údajoch o súkromí v App Store.