Jak zrobić formularz zapytania ofertowego na Androida w Kotlinie
Formularz zapytania ofertowego w Jetpack Compose z wyborem usługi i budżetu oraz terminem, który wysyła Ci każde zapytanie e-mailem, a klientowi automatyczne potwierdzenie.
Zaktualizowano:
W tym samouczku zbudujesz ekran zapytania ofertowego w Jetpack Compose. Klient wybiera usługę i budżet, wpisuje termin i opis; Ty dostajesz uporządkowany e-mail, a klient automatyczne potwierdzenie.
Potrzebujesz: darmowego konta Frontmail, projektu na Androida z Compose i około dwudziestu minut.
1. Podłącz skrzynkę i utwórz szablony
- Usługi e-mail → Dodaj usługę: podłącz swoją skrzynkę lub dostawcę, Ustaw jako domyślną, Zweryfikuj i zapisz.
- Szablony → Z galerii → Zapytanie ofertowe (kategoria Biznes) → Użyj szablonu.
- Z galerii → Automatyczna odpowiedź: dziękujemy za wiadomość → Użyj szablonu; ustaw
wartości domyślne
company_nameiresponse_time, a potem wybierz go w szablonie zapytania w Ustawienia → Doręczanie → Szablon automatycznej odpowiedzi.
Szablon zapytania wymaga name, email, service i details; phone, company, budget
i deadline (YYYY-MM-DD) są opcjonalne. service i budget przyjmują tylko wartości z zakładki
Parametry. Przykładowy kod używa wartości z angielskiej wersji szablonu; szablon utworzony po
polsku ma Strona internetowa, Sklep internetowy, Aplikacja mobilna, Konsultacje, Inne oraz Do
20 tys. zł, 20–100 tys. zł, 100–200 tys. zł, Powyżej 200 tys. zł – aplikacja musi wysyłać
dokładnie te same wartości.
2. Zezwól na aplikacje mobilne i włącz CAPTCHA
- Bezpieczeństwo → Aplikacje mobilne → Zezwól na aplikacje mobilne → Zapisz.
- Szablon zapytania → Ustawienia → Ochrona → Wymagaj CAPTCHA (Turnstile) (wspólny mobilny widżet Frontmail, klucze nie są potrzebne).
3. Dodaj SDK
// app/build.gradle.kts
dependencies {
implementation("dev.frontmail:frontmail:0.1.0")
}
Dodaj uprawnienie INTERNET i skopiuj funkcje pomocnicze showTurnstile / resetTurnstile
z sekcji o Turnstile w dokumentacji Android SDK.
4. Zbuduj ekran
import android.webkit.WebView
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import dev.frontmail.Frontmail
import dev.frontmail.FrontmailException
import dev.frontmail.SendOptions
import dev.frontmail.Turnstile
import dev.frontmail.TurnstileWidget
import kotlinx.coroutines.launch
import java.time.LocalDate
val frontmail = Frontmail("pk_…")
val services = listOf("Website", "E-shop", "Mobile app", "Consulting", "Other")
val budgets = listOf("Under $5k", "$5k–20k", "$20k–50k", "Over $50k")
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun Choices(options: List<String>, selected: String?, onSelect: (String?) -> Unit) {
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
options.forEach { option ->
FilterChip(
selected = option == selected,
onClick = { onSelect(if (option == selected) null else option) },
label = { Text(option) },
)
}
}
}
@Composable
fun QuoteScreen() {
var name by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
var service by remember { mutableStateOf<String?>(services.first()) }
var budget by remember { mutableStateOf<String?>(null) }
var deadline by remember { mutableStateOf("") }
var details by remember { mutableStateOf("") }
var widget by remember { mutableStateOf<TurnstileWidget?>(null) }
var token by remember { mutableStateOf<String?>(null) }
var webView by remember { mutableStateOf<WebView?>(null) }
var info by remember { mutableStateOf("") }
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) { widget = Turnstile.sharedWidget() }
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedTextField(name, { name = it }, label = { Text("Name") }, modifier = Modifier.fillMaxWidth())
OutlinedTextField(email, { email = it }, label = { Text("Email") }, modifier = Modifier.fillMaxWidth())
Text("What do you need?")
Choices(services, service) { service = it }
Text("Budget (optional)")
Choices(budgets, budget) { budget = it }
OutlinedTextField(deadline, { deadline = it }, label = { Text("Deadline (YYYY-MM-DD, optional)") }, modifier = Modifier.fillMaxWidth())
OutlinedTextField(details, { details = it }, label = { Text("Project details") }, minLines = 4, modifier = Modifier.fillMaxWidth())
widget?.let { w ->
AndroidView(
factory = { context ->
WebView(context).apply {
showTurnstile(w, onToken = { token = it }, onExpire = { token = null })
webView = this
}
},
modifier = Modifier.fillMaxWidth().height(70.dp),
)
}
Button(
enabled = token != null && service != null,
onClick = {
scope.launch {
try {
frontmail.send(
templateId = "tpl_…",
params = mapOf(
"name" to name,
"email" to email,
"service" to service,
"budget" to budget,
"deadline" to runCatching { LocalDate.parse(deadline).toString() }.getOrNull(),
"details" to details,
),
options = SendOptions(turnstileToken = token, turnstileKey = widget?.turnstileKey),
)
info = "Thanks! We’ve emailed you a confirmation."
} catch (e: FrontmailException) {
info = e.message ?: "Sending failed"
} finally {
token = null // a token works only once
webView?.resetTurnstile()
}
}
},
) { Text("Request a quote") }
Text(info)
}
}
Wartości null (brak budżetu, brak poprawnego terminu) są pomijane w e-mailu. Jeśli wolisz, zamień
pole tekstowe na DatePicker z Material 3 – wystarczy, że wyślesz datę jako YYYY-MM-DD.
5. Przetestuj
Wyślij zapytanie ze swoim adresem e-mail: dostaniesz zapytanie, a chwilę później potwierdzenie. Oba pojawią się w Historii z osią czasu doręczenia.
Zapytanie z potwierdzeniem zużywa dwa kredyty. Jeśli kredyty się skończą, zapytania są przyjmowane jako
HELDi wysyłane automatycznie po doładowaniu.
Co dalej
- Włącz Zbieranie kontaktów w ustawieniach szablonu, żeby mieć listę wszystkich, którzy prosili o wycenę.
- Dołączaj pliki (na przykład brief w PDF) przez
SendOptions(attachments = …)– szczegóły w dokumentacji SDK.


