Saltar al contenido

Cómo crear un formulario de contacto en React Native (y Expo) que envíe correos

Un formulario de contacto en React Native que te envía cada mensaje por correo, con estado de envío, protección contra bots y sin backend. Funciona en Expo y en React Native puro.

Actualizado:

El vídeo está en inglés; los pasos de abajo son el tutorial completo.

Vas a añadir un formulario de contacto a una app de React Native o Expo con el paquete @frontmail/react-native: un provider, un hook useSendEmail que controla el estado del envío y un componente de Turnstile listo para usar contra los bots. El correo sale a través de tu propio buzón.

Necesitas: una cuenta gratuita de Frontmail, un proyecto de Expo o React Native y unos diez minutos.

1. Conecta tu buzón y crea la plantilla

  1. Servicios de correo → Añadir servicio: conecta Gmail, Outlook, SMTP o un proveedor transaccional, activa Marcar como predeterminado y pulsa Verificar y guardar.
  2. Plantillas → Desde la galería → Formulario de contacto → Usar plantilla. Parámetros obligatorios: name, email, message; opcionales: phone, topic.
  3. Copia el ID de la plantilla (tpl_…) de la pestaña Fragmentos.

2. Permite las apps móviles

  • Seguridad → Apps móviles → Permitir apps móviles → Guardar. Las apps no envían cabecera Origin, así que sin este interruptor la API responde origin_not_allowed.
  • Plantilla → Ajustes → Protección → Exigir CAPTCHA (Turnstile). La app usa el widget compartido de Turnstile de Frontmail; no necesitas claves de Cloudflare.

3. Instala

npx expo install @frontmail/react-native @react-native-async-storage/async-storage react-native-webview

En un proyecto de React Native sin Expo, usa npm i con los mismos paquetes y ejecuta npx pod-install.

4. Crea el formulario

import { useRef, useState } from 'react';
import { Button, Text, TextInput, View } from 'react-native';
import { FrontmailProvider, TurnstileWebView, useSendEmail, type TurnstileWebViewHandle } from '@frontmail/react-native';

const options = { publicKey: 'pk_…' }; // keep the object stable

export default function App() {
  return (
    <FrontmailProvider options={options}>
      <ContactForm />
    </FrontmailProvider>
  );
}

function ContactForm() {
  const { send, status, error } = useSendEmail(null, 'tpl_…'); // null = default service
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [message, setMessage] = useState('');
  const [token, setToken] = useState<string>();
  const turnstile = useRef<TurnstileWebViewHandle>(null);

  const onSend = async () => {
    await send({ name, email, message }, { turnstileToken: token });
    setToken(undefined);
    turnstile.current?.reset(); // a token works only once
  };

  if (status === 'sent' || status === 'held') return <Text>Thank you! We’ll get back to you soon.</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" />
      <TextInput placeholder="Message" value={message} onChangeText={setMessage} multiline />
      <TurnstileWebView ref={turnstile} onToken={setToken} />
      <Button title="Send" disabled={!token || status === 'sending'} onPress={onSend} />
      {error && <Text>{error.message}</Text>}
    </View>
  );
}

send nunca lanza excepciones: se resuelve con el resultado o con undefined, y status pasa de idle a sending y después a sent, held o error. Los reintentos tras un error de red reutilizan una misma clave de idempotencia, así que un usuario en el tren nunca te envía el mismo mensaje dos veces.

5. Pruébalo

Arranca la app (npx expo start), rellena el formulario y pulsa Send. El correo llega a tu bandeja y aparece en Historial en el panel.

  • origin_not_allowed – Permitir apps móviles está desactivado o no se guardó.
  • captcha_required – la verificación de Turnstile aún no ha terminado; el botón sigue desactivado hasta que haya un token.
  • invalid_template_params – un campo obligatorio está vacío o el correo no es válido.

held significa que el mensaje se aceptó cuando ya habías gastado tus créditos. Sale automáticamente en cuanto recargas: no se descarta nada.

Próximos pasos

  • Añade limitRate: { id: 'contact', throttle: 30_000 } a las opciones del provider para permitir un mensaje cada 30 segundos por dispositivo.
  • Envía una confirmación al usuario con la plantilla Respuesta automática: gracias por tu mensaje de la galería.
Todos los tutoriales