Cómo crear un formulario de solicitud de presupuesto en React Native
Un formulario de presupuesto en React Native y Expo con opciones de servicio, presupuesto y fecha límite que te envía cada solicitud por correo y se la confirma automáticamente al cliente.
Actualizado:
En este tutorial crearás una pantalla de solicitud de presupuesto en React Native: el cliente elige un servicio y un presupuesto, añade una fecha límite y una descripción, y tú recibes un correo estructurado. Una respuesta automática le confirma la solicitud al cliente.
Necesitas: una cuenta gratuita de Frontmail, un proyecto de Expo o React Native y unos quince minutos.
1. Conecta tu buzón y crea las plantillas
- Servicios de correo → Añadir servicio: conecta tu buzón o proveedor, Marcar como predeterminado, Verificar y guardar.
- Plantillas → Desde la galería → Solicitud de presupuesto (categoría Negocios) → Usar plantilla.
- Desde la galería → Respuesta automática: gracias por tu mensaje → Usar plantilla.
Define valores por defecto para
company_nameyresponse_timey elígela en la plantilla de presupuesto en Ajustes → Entrega → Plantilla de respuesta automática.
Parámetros de la plantilla de presupuesto: name, email, service, details (obligatorios) y
phone, company, budget, deadline (opcionales, la fecha límite como YYYY-MM-DD).
service y budget solo aceptan los valores de la pestaña Parámetros. El código de ejemplo
usa los de la plantilla en inglés; si creas la plantilla en español, son Sitio web, Tienda
online, App móvil, Consultoría, Otro y Menos de 5000 €, 5000–20 000 €, 20 000–50 000 €, Más de
50 000 €. Usa en la app exactamente los mismos textos.
2. Permite las apps móviles y activa la CAPTCHA
- Seguridad → Apps móviles → Permitir apps móviles → Guardar.
- Plantilla de presupuesto → Ajustes → Protección → Exigir CAPTCHA (Turnstile): la app usa el widget compartido de Frontmail, sin necesidad de claves.
3. Instala
npx expo install @frontmail/react-native @react-native-async-storage/async-storage react-native-webview
4. Crea el formulario
Para que el ejemplo no necesite dependencias extra, las opciones son simples botones tipo chip y
la fecha límite se escribe como YYYY-MM-DD. Cámbialos por tus componentes de selección
favoritos: lo único que importa son los valores que pasas a 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>
);
}
Los valores opcionales vacíos (sin presupuesto, fecha límite vacía) se omiten automáticamente en el correo.
5. Pruébalo
Ejecuta la app y envía una solicitud con tu propio correo. Recibirás la solicitud y la
confirmación; ambas aparecen en Historial. Los errores de validación llegan como
invalid_template_params, con el campo que ha fallado en error.details.
Una solicitud con confirmación consume dos créditos. Cuando se acaban los créditos, las solicitudes se aceptan como
heldy se envían en cuanto recargas; el hook devuelvestatus === 'held'.
Próximos pasos
- Guarda una lista de todas las personas que han preguntado: activa Recogida de contactos en los ajustes de la plantilla.
- Pasa
{ locale: 'de' }como segundo argumento desendpara usar una variante de idioma de la plantilla.


