Przejdź do treści

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:

Film jest po angielsku; poniżej znajdziesz pełny samouczek krok po kroku.

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

  1. Usługi e-mail → Dodaj usługę: podłącz swoją skrzynkę lub dostawcę, Ustaw jako domyślną, Zweryfikuj i zapisz.
  2. Szablony → Z galerii → Zapytanie ofertowe (kategoria Biznes) → Użyj szablonu.
  3. Z galerii → Automatyczna odpowiedź: dziękujemy za wiadomość → Użyj szablonu. Ustaw wartości domyślne company_name i response_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 held i wysyłane po doładowaniu – hook zwraca wtedy status === 'held'.

Co dalej

  • Zbieraj listę wszystkich pytających: włącz Zbieranie kontaktów w ustawieniach szablonu.
  • Przekaż { locale: 'de' } jako drugi argument send, żeby użyć wersji językowej szablonu.
Wszystkie samouczki