Přeskočit na obsah

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.

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.

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 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
]
)
)

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", …]

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 SwiftUI
import WebKit
import 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á.

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.

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.

  • 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 chybou private_key_in_browser. Viz Public key a private key.
  • Se zapnutým Povolit mobilní aplikace přijmeme požadavky bez hlavičky Origin odkudkoli – 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.

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.