Ir al contenido

React Native (@frontmail/react-native)

@frontmail/react-native permite que una app de iOS o Android envíe correos a través de tus plantillas con la clave pública, sin necesidad de un backend propio. Funciona con Expo y con React Native sin Expo (bare).

Expo:

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

React Native sin Expo:

Ventana de terminal
npm install @frontmail/react-native @react-native-async-storage/async-storage react-native-webview
npx pod-install

Los dos paquetes adicionales son dependencias peer opcionales:

Paquete Necesario para
@react-native-async-storage/async-storage Un limitRate que sobreviva a los reinicios de la app
react-native-webview <TurnstileWebView>

Las apps nativas no envían cabecera Origin ni Referer, así que las solicitudes desde la app se rechazan (tengas o no configurados sitios web permitidos) con 403 origin_not_allowed hasta que actives Seguridad → Apps móviles → Permitir apps móviles en el panel y hagas clic en Guardar. Consulta Ajustes de seguridad.

Las solicitudes desde sitios web se siguen comprobando contra la lista, así que el interruptor no debilita la protección de tus formularios web. El interruptor es necesario incluso con la lista vacía.

Envuelve la app en FrontmailProvider y envía con useSendEmail. Mantén estable el objeto de opciones: defínelo a nivel de módulo o con useMemo:

import { useState } from 'react';
import { Button, Text, TextInput, View } from 'react-native';
import { FrontmailProvider, useSendEmail } from '@frontmail/react-native';
const frontmailOptions = { publicKey: 'pk_4f2a…', limitRate: { id: 'contact', throttle: 30_000 } };
export default function App() {
return (
<FrontmailProvider options={frontmailOptions}>
<ContactScreen />
</FrontmailProvider>
);
}
function ContactScreen() {
const { send, status, error } = useSendEmail('svc_01J9…', 'tpl_contact');
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
return (
<View>
<TextInput value={email} onChangeText={setEmail} keyboardType="email-address" autoCapitalize="none" />
<TextInput value={message} onChangeText={setMessage} multiline />
<Button title="Enviar" disabled={status === 'sending'} onPress={() => send({ email, message })} />
{status === 'sent' && <Text>¡Gracias!</Text>}
{status === 'held' && <Text>Recibido: lo entregaremos en breve.</Text>}
{error && <Text>{error.message}</Text>}
</View>
);
}

options acepta publicKey, apiUrl, retry, limitRate, storageProvider y blockList. No se acepta privateKey. También puedes pasar un cliente existente: <FrontmailProvider client={client}>.

useSendEmail(serviceId, templateId) devuelve { send, getStatus, status, error, result, reset }. status pasa de idle → sending → sent | held | error. send(params, options?) se resuelve con el resultado, o con undefined si hay un error (el error queda en error); nunca se rechaza, así que no necesitas try/catch. reset() devuelve el hook a idle.

Para enviar en un idioma de la plantilla, pasa locale en las opciones: send({ email, message }, { locale: 'de' }), por ejemplo con el idioma del dispositivo de Intl.DateTimeFormat().resolvedOptions().locale. Los idiomas desconocidos recurren al mismo idioma base y, si no, al idioma predeterminado de la plantilla; result.locale es el idioma usado.

Todas las solicitudes llevan la cabecera X-Frontmail-Client: @frontmail/react-native/<version>.

Si la plantilla exige Turnstile, renderiza <TurnstileWebView> y pasa el token a send. No necesitas site key: por defecto, el componente usa la clave móvil compartida de Frontmail. La obtiene de GET /v1/public-config de la API configurada en el provider (en caché, así que solo se carga una vez) y muestra el widget de Cloudflare en react-native-webview como una página HTML incrustada con la URL https://mobile.frontmail.dev; desde esa dirección no se carga nada.

import { useRef, useState } from 'react';
import { Button } from 'react-native';
import { TurnstileWebView, useSendEmail } from '@frontmail/react-native';
import type { TurnstileWebViewHandle } from '@frontmail/react-native';
function ContactScreen() {
const { send, status } = useSendEmail('svc_01J9…', 'tpl_contact');
const turnstile = useRef<TurnstileWebViewHandle>(null);
const [token, setToken] = useState<string>();
const onSubmit = async () => {
await send({ email, message }, { turnstileToken: token });
// Los tokens son de un solo uso: obtén uno nuevo para el siguiente intento.
setToken(undefined);
turnstile.current?.reset();
};
return (
<>
{/* …campos… */}
<TurnstileWebView
ref={turnstile}
onToken={setToken}
onExpire={() => setToken(undefined)}
onError={(e) => console.warn(e.message)}
/>
<Button title="Enviar" disabled={!token || status === 'sending'} onPress={onSubmit} />
</>
);
}

Props: onToken y, opcionalmente, siteKey, baseUrl, onError, onExpire, theme (auto | light | dark), size (normal | compact | flexible), action, language, style y webViewProps (se pasan al WebView).

El WebView está limitado al widget: solo carga la página incrustada (baseUrl) y el desafío de Cloudflare, los enlaces dentro del widget se abren en el navegador del sistema y solo los mensajes de la página del widget llegan a tus callbacks. webViewProps no puede sobrescribir source, originWhitelist, la protección de navegación, onMessage ni los ajustes de JavaScript y de acceso a archivos.

Las solicitudes de la app no llevan cabecera Origin, así que Frontmail verifica sus tokens con su secreto móvil compartido. No necesitas tus propias claves de Turnstile para esto: con Permitir apps móviles activado, puedes activar la CAPTCHA en las plantillas de la app aunque tu organización todavía no tenga claves.

Para usar en su lugar tu propio widget de Turnstile, pasa tanto siteKey como baseUrl:

<TurnstileWebView
ref={turnstile}
siteKey="YOUR_TURNSTILE_SITE_KEY"
baseUrl="https://example.com"
onToken={setToken}
/>

El SDK envía entonces automáticamente los tokens de este widget con turnstile_key: "org", y Frontmail los verifica con la secret key de Seguridad → Protección contra bots (Turnstile).

Un token es válido durante unos 5 minutos y se puede usar una sola vez: llama a ref.current.reset() después de cada envío, tenga éxito o no.

limitRate limita los envíos en el dispositivo, p. ej. como máximo un mensaje cada 30 segundos. En React Native no existe location.pathname, así que establece siempre limitRate.id:

import AsyncStorage from '@react-native-async-storage/async-storage';
import { asyncStorageProvider } from '@frontmail/react-native';
const frontmailOptions = {
publicKey: 'pk_4f2a…',
limitRate: { id: 'contact', throttle: 30_000 },
storageProvider: asyncStorageProvider(AsyncStorage),
};

Sin storageProvider, el SDK usa AsyncStorage cuando está instalado; si no, guarda el límite solo en memoria (se reinicia al reiniciar la app) y muestra una advertencia en desarrollo. Usa memoryStorageProvider() para elegir explícitamente el almacenamiento en memoria.

limitRate solo protege contra toques repetidos por accidente; la protección real está en el servidor (consulta Seguridad).

getStatus() del hook lee el estado de entrega del último mensaje aceptado. Para todo lo demás, useFrontmail() devuelve el cliente del provider (send, getStatus):

import { useFrontmail } from '@frontmail/react-native';
const client = useFrontmail();
const status = await client.getStatus(messageId, { token });

Los errores de red, los 5xx y los 429 se reintentan automáticamente, y todos los intentos de un mismo envío reutilizan la misma clave de idempotencia, así que un reintento nunca envía el correo dos veces. Las claves son UUID v4: el SDK usa crypto.randomUUID y, en Hermes, que no lo ofrece, recurre a su propio generador. Consulta Reintentos e idempotencia.

Con los tipos generados (npx frontmail types amplía @frontmail/react-native), useSendEmail('svc_…', 'tpl_contact').send({...}) comprueba los tipos de los parámetros de esa plantilla.

error es un FrontmailError con code, status y message. Cómo tratar cada código se explica en Gestión de errores.

  • En una app pon siempre solo la clave pública (pk_…). Todo lo que hay en el bundle de una app se puede extraer, así que una clave privada ahí es como si estuviera publicada. Consulta Claves públicas y privadas.
  • Con Permitir apps móviles activado, las solicitudes sin cabecera Origin se aceptan desde cualquier lugar: cualquier script que conozca tu clave pública puede enviarlas. Apóyate en el límite de solicitudes por IP, en Turnstile en todas las plantillas que use la app y en la lista de bloqueo.

Proyecto de ejemplo: examples/react-native.