Jak zrobić formularz zapytania ofertowego w React Native
Formularz zapytania ofertowego w React Native i Expo z wyborem usługi, budżetu i terminu, który wysyła Ci każde zapytanie e-mailem i automatycznie potwierdza je klientowi.
Zaktualizowano:
W tym samouczku zbudujesz w React Native ekran zapytania ofertowego: klient wybiera usługę i budżet, dodaje termin i opis, a Ty dostajesz uporządkowany e-mail. Automatyczna odpowiedź potwierdza klientowi, że zapytanie dotarło.
Potrzebujesz: darmowego konta Frontmail, projektu w Expo lub React Native i około piętnastu 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.
Parametry szablonu zapytania: name, email, service, details (wymagane) oraz phone,
company, budget, deadline (opcjonalne, termin jako YYYY-MM-DD). service i budget
przyjmują tylko wartości podane w zakładce 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) – aplikacja korzysta ze wspólnego widżetu Frontmail, klucze nie są potrzebne.
3. Instalacja
npx expo install @frontmail/react-native @react-native-async-storage/async-storage react-native-webview
4. Zbuduj formularz
Żeby przykład obszedł się bez dodatkowych zależności, wybór to proste przyciski-chipy, a termin
wpisuje się jako YYYY-MM-DD. Możesz podmienić je na swoje ulubione komponenty pickerów – liczą
się tylko wartości, które przekazujesz do send.
import { useRef, useState } from 'react';
import { Button, Pressable, Text, TextInput, View } from 'react-native';
import { FrontmailProvider, TurnstileWebView, useSendEmail, type TurnstileWebViewHandle } from '@frontmail/react-native';
const options = { publicKey: 'pk_…' };
const SERVICES = ['Website', 'E-shop', 'Mobile app', 'Consulting', 'Other'];
const BUDGETS = ['Under $5k', '$5k–20k', '$20k–50k', 'Over $50k'];
export default function App() {
return (
<FrontmailProvider options={options}>
<QuoteForm />
</FrontmailProvider>
);
}
function Choice({ values, value, onChange }: { values: string[]; value?: string; onChange(v?: string): void }) {
return (
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>
{values.map((v) => (
<Pressable key={v} onPress={() => onChange(v === value ? undefined : v)} accessibilityState={{ selected: v === value }}>
<Text style={{ padding: 8, borderWidth: 1, borderRadius: 16, fontWeight: v === value ? '700' : '400' }}>{v}</Text>
</Pressable>
))}
</View>
);
}
function QuoteForm() {
const { send, status, error } = useSendEmail(null, 'tpl_…');
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [service, setService] = useState<string | undefined>(SERVICES[0]);
const [budget, setBudget] = useState<string>();
const [deadline, setDeadline] = useState('');
const [details, setDetails] = useState('');
const [token, setToken] = useState<string>();
const turnstile = useRef<TurnstileWebViewHandle>(null);
const onSend = async () => {
await send({ name, email, service, budget, deadline, details }, { turnstileToken: token });
setToken(undefined);
turnstile.current?.reset(); // a token works only once
};
if (status === 'sent' || status === 'held') return <Text>Thanks! We’ve emailed you a confirmation.</Text>;
return (
<View style={{ gap: 12, padding: 16 }}>
<TextInput placeholder="Name" value={name} onChangeText={setName} />
<TextInput placeholder="Email" value={email} onChangeText={setEmail} keyboardType="email-address" autoCapitalize="none" />
<Text>What do you need?</Text>
<Choice values={SERVICES} value={service} onChange={setService} />
<Text>Budget (optional)</Text>
<Choice values={BUDGETS} value={budget} onChange={setBudget} />
<TextInput placeholder="Deadline (YYYY-MM-DD, optional)" value={deadline} onChangeText={setDeadline} />
<TextInput placeholder="Project details" value={details} onChangeText={setDetails} multiline />
<TurnstileWebView ref={turnstile} onToken={setToken} />
<Button title="Request a quote" disabled={!token || !service || status === 'sending'} onPress={onSend} />
{error && <Text>{error.message}</Text>}
</View>
);
}
Puste wartości opcjonalne (brak budżetu, pusty termin) są automatycznie pomijane w e-mailu.
5. Przetestuj
Uruchom aplikację i wyślij zapytanie ze swoim adresem e-mail. Dostaniesz zapytanie i potwierdzenie;
oba pojawią się w Historii. Błędy walidacji wracają jako invalid_template_params, a pole,
które nie przeszło, znajdziesz w error.details.
Zapytanie z potwierdzeniem zużywa dwa kredyty. Gdy kredyty się skończą, zapytania są przyjmowane jako
heldi wysyłane po doładowaniu – hook zwraca wtedystatus === 'held'.
Co dalej
- Zbieraj listę wszystkich pytających: włącz Zbieranie kontaktów w ustawieniach szablonu.
- Przekaż
{ locale: 'de' }jako drugi argumentsend, żeby użyć wersji językowej szablonu.


