iOS a platformy Apple (Swift)
Swift balíček Frontmail umožní aplikaci pro iOS (i pro ostatní platformy Apple) odesílat e-maily
přes vaše šablony s public key – bez vlastního backendu. Nemá žádné závislosti, používá
async/await a překládá se v jazykovém režimu Swift 6.
Požadavky: Swift 5.9+ (Xcode 15+); iOS 15, macOS 12, tvOS 15, watchOS 8, visionOS 1.
Instalace
Sekce “Instalace”Xcode: File → Add Package Dependencies… → zadejte
https://github.com/frontmail-dev/frontmail-swift → knihovnu Frontmail přidejte do targetu
aplikace.
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")]),]Povolte mobilní aplikace
Sekce “Povolte mobilní aplikace”Nativní aplikace neposílají hlavičku Origin ani Referer, takže požadavky z aplikace (bez ohledu
na to, jestli máte nastavené povolené weby) skončí chybou
403 origin_not_allowed, dokud v dashboardu nezapnete
Zabezpečení → Mobilní aplikace → Povolit mobilní aplikace a nekliknete na Uložit. Viz
Nastavení zabezpečení.
Požadavky z webů se dál kontrolují podle seznamu, takže přepínač ochranu vašich webových formulářů neoslabí. Přepínač je potřeba i s prázdným seznamem.
Odesílání
Sekce “Odesílání”Klienta vytvořte jednou (je neměnný a dá se sdílet mezi vlákny) a volejte 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ý den!", "qty": 2, "items": [["sku": "A-1"]]], serviceID: "svc_01J9…" // volitelné – nil použije výchozí službu šablony)if result.status == .held { // Přijato – doručí se za chvíli (organizace čeká na kredity).}send vrací SendResult s vlastnostmi messageID, status (.queued nebo .held)
a statusToken. Když předáte private key, inicializátor vyhodí chybu (viz
Zabezpečení).
Volby inicializátoru:
let frontmail = try Frontmail( publicKey: "pk_4f2a…", apiURL: URL(string: "https://api.frontmail.dev")!, // výchozí retry: RetryPolicy(maxRetries: 3, baseDelay: 0.3, maxDelay: 10), // výchozí; .disabled = bez opakování timeout: 15, // sekundy na jeden pokus (výchozí) session: .shared // URLSession (výchozí))Každý požadavek nese hlavičku X-Frontmail-Client: frontmail-swift/<verze>.
Parametry
Sekce “Parametry”Parametry jsou [String: FrontmailValue] – hodnota JSON, kterou zapíšete běžnými literály
(řetězce, čísla, booleany, nil, pole a slovníky). Hodnoty z proměnných zabalte do příslušného
případu: ["email": .string(email)].
Poslouží i libovolná hodnota Encodable, ze které vznikne objekt JSON. Názvy vlastností se pošlou
beze změny (přejmenujete je přes CodingKeys), data jako řetězce ISO-8601:
struct Contact: Encodable { let email: String let message: String}
try await frontmail.send(templateID: "tpl_contact", params: Contact(email: email, message: message))Přílohy a další volby
Sekce “Přílohy a další volby”SendOptions nese token Turnstile, přílohy a případně vlastní idempotency key:
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_…"), // nahráno dřív přes POST /v1/uploads ] ))Stav doručení
Sekce “Stav doručení”S statusToken z výsledku send si aplikace přečte stav dané zprávy. SDK token posílá
v hlavičce 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, … nebo .unknown("…")print(status.events.map(\.type)) // ["accepted", "sent", …]Turnstile
Sekce “Turnstile”Pokud šablona vyžaduje Turnstile, zobrazte widget Cloudflare ve
WKWebView a token předejte do send. Site key nepotřebujete: ve výchozím stavu se použije
sdílený mobilní klíč Frontmailu. Turnstile.widget(client:) ho načte z
GET /v1/public-config (s cache v paměti, takže jen
jednou) a widget běží jako vložená HTML stránka s adresou https://mobile.frontmail.dev – z té
adresy se nic nestahuje.
SDK dodává jednotlivé díly, samotné WebView patří do vaší aplikace:
| API | Co dělá |
|---|---|
Turnstile.widget(client:) |
site key + adresa stránky + turnstileKey, se kterým se tokeny posílají |
Turnstile.html(siteKey:theme:size:action:language:) |
stránka s widgetem; zprávy posílá do script message handleru frontmailTurnstile |
Turnstile.parseMessage(_:) |
ověří zprávu → .token(String), .expired, .error(code:) |
Turnstile.navigationAction(url:baseURL:isTopFrame:) |
hlídání navigace: .allow, .cancel, .openExternally(URL) |
Turnstile.isTrustedMessageOrigin(_:baseURL:) |
přijme zprávy jen ze stránky widgetu |
Turnstile.resetScript |
JavaScript, který widget resetuje a vyžádá nový token |
Kompletní view pro SwiftUI – zkopírujte si ho do aplikace:
import SwiftUIimport WebKitimport Frontmail
struct TurnstileView: UIViewRepresentable { let widget: Turnstile.Widget var theme: Turnstile.Theme = .auto var size: Turnstile.Size = .normal /// Změnou hodnoty widget resetujete a získáte nový token (tokeny jdou použít jen jednou). 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 aplikací smí mluvit jen 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 zůstane na widgetu, odkazy z něj se otevřou v systémovém prohlížeč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ářet. 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žití ve formuláři: získat token, odeslat a widget resetovat:
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("Zprá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("Odeslat") { 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 jde použít jen jednou – nový si vyžádejte bez ohledu na výsledek. 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 ? "Přijato – doručíme za chvíli." : "Děkujeme!" } catch let error as FrontmailError { info = error.message } catch { info = "\(error)" } }}WebView je uzamčené na widget: načte jen vloženou stránku (baseURL) a výzvu Cloudflaru, odkazy
ve widgetu se otevřou v systémovém prohlížeči, druhé okno otevřít nejde a do vašeho kódu se dostanou
jen zprávy ze stránky widgetu. Stejný Coordinator funguje i v UIKitu.
Požadavky z aplikace nemají hlavičku Origin, takže jejich tokeny Frontmail ověří svým sdíleným
mobilním tajným klíčem. Vlastní klíče Turnstile k tomu nepotřebujete – se zapnutým Povolit
mobilní aplikace můžete CAPTCHA u šablon aplikace zapnout, i když organizace klíče zatím nemá.
Vlastní site key
Sekce “Vlastní site key”Pokud chcete místo toho použít vlastní widget Turnstile, vytvořte 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 ověří secret key ze Zabezpečení → Ochrana proti
botům (Turnstile).
Token platí asi 5 minut a jde použít jen jednou: po každém odeslání, úspěšném i neúspěšném, widget resetujte.
Opakování a idempotence
Sekce “Opakování a idempotence”Chyby sítě, vypršení časového limitu, 5xx a 429 se automaticky opakují (ve výchozím stavu
třikrát, s exponenciálním čekáním a náhodným rozptylem). SDK respektuje hlavičku Retry-After;
pokud žádá víc než 60 sekund, chybu vyhodí hned. Ostatní odpovědi 4xx se nikdy neopakují. Všechny
pokusy jednoho odeslání sdílejí stejný idempotency key (náhodné UUID, pokud nepředáte vlastní
idempotencyKey), takže opakování e-mail nikdy nepošle dvakrát. Zrušení volajícího Task požadavek
okamžitě ukončí s kódem aborted. Viz Retry a idempotence.
Chyby
Sekce “Chyby”Všechno vyhazuje FrontmailError s vlastnostmi code, message, status (nil u chyb na straně
klienta), docsURL, details a retryAfter. Klientské kódy jsou network_error, timeout,
aborted, invalid_response a private_key_in_browser; kontroly isAuth, isValidation,
isRateLimit, isInsufficientCredits, isNetwork a isCancelled je seskupují:
do { try await frontmail.send(templateID: "tpl_contact", params: params)} catch let error as FrontmailError where error.isRateLimit { show("Příliš mnoho zpráv – zkuste to znovu za \(Int(error.retryAfter ?? 60)) s.")} catch let error as FrontmailError where error.code == .originNotAllowed { assertionFailure("V dashboardu zapněte Zabezpečení → Mobilní aplikace → Povolit mobilní aplikace.")} catch let error as FrontmailError { show(error.message)}Jak reagovat na jednotlivé kódy, popisuje Zpracování chyb.
Zabezpečení
Sekce “Zabezpečení”- Do aplikace patří jen public key (
pk_…). Cokoli v balíčku aplikace se dá vytáhnout, takže private key v ní je jako zveřejněný. Klíče začínajícísk_SDK odmítne s chybouprivate_key_in_browser. Viz Public key a private key. - Se zapnutým Povolit mobilní aplikace přijmeme požadavky bez hlavičky
Originodkudkoli – poslat je může jakýkoli skript, který zná váš public key. Spolehněte se na limit na IP adresu, Turnstile u všech šablon, které aplikace používá, a na seznam blokovaných hodnot. - Šablona, která bere příjemce z parametrů (třeba potvrzení na adresu, kterou uživatel zadal), je uzamčená: potřebuje Turnstile, pevného odesílatele, krátké parametry bez HTML a odkazy jen na vaše domény. Viz Povolené odkazy a zámek obsahu.
Soukromí
Sekce “Soukromí”SDK uživatele nesleduje, samo žádná data nesbírá a nepoužívá API, u kterých Apple vyžaduje uvést
důvod – totéž říká jeho PrivacyInfo.xcprivacy. To, co posílají vaše šablony (například e-mailová
adresa), jsou data, která sbírá vaše aplikace: uveďte je v jejím privacy manifestu a v údajích
o soukromí v App Storu.