Salta ai contenuti

React Native (@frontmail/react-native)

@frontmail/react-native permette a un’app iOS o Android di inviare email tramite i tuoi modelli con la chiave pubblica – senza bisogno di un tuo backend. Funziona con Expo e con React Native «bare».

Expo:

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

React Native bare:

Finestra del terminale
npm install @frontmail/react-native @react-native-async-storage/async-storage react-native-webview
npx pod-install

I due pacchetti aggiuntivi sono peer dependency facoltative:

Pacchetto Serve per
@react-native-async-storage/async-storage limitRate che sopravvive ai riavvii dell’app
react-native-webview <TurnstileWebView>

Le app native non inviano l’header Origin né Referer, quindi le richieste dall’app vengono rifiutate (che tu abbia impostato o meno dei siti consentiti) con 403 origin_not_allowed finché non attivi Sicurezza → App mobile → Consenti app mobile nella dashboard e fai clic su Salva. Vedi Impostazioni di sicurezza.

Le richieste dai siti web vengono comunque confrontate con la lista, quindi l’interruttore non indebolisce la protezione dei tuoi moduli web. L’interruttore è necessario anche con la lista vuota.

Avvolgi l’app in FrontmailProvider e invia con useSendEmail. Mantieni stabile l’oggetto delle opzioni – definiscilo a livello di modulo 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="Invia" disabled={status === 'sending'} onPress={() => send({ email, message })} />
{status === 'sent' && <Text>Grazie!</Text>}
{status === 'held' && <Text>Ricevuto – verrà recapitato a breve.</Text>}
{error && <Text>{error.message}</Text>}
</View>
);
}

options accetta publicKey, apiUrl, retry, limitRate, storageProvider e blockList. Una privateKey non è accettata. In alternativa, passa un client esistente: <FrontmailProvider client={client}>.

useSendEmail(serviceId, templateId) restituisce { send, getStatus, status, error, result, reset }. status è idle → sending → sent | held | error. send(params, options?) si risolve con il risultato oppure con undefined in caso di errore (l’errore è in error) – non viene mai rifiutata, quindi non ti serve try/catch. reset() riporta l’hook a idle.

Per inviare in una lingua del modello, passa locale nelle opzioni: send({ email, message }, { locale: 'de' }) – ad esempio la lingua del dispositivo da Intl.DateTimeFormat().resolvedOptions().locale. Le lingue sconosciute ripiegano sulla stessa lingua di base, poi sulla lingua predefinita del modello; result.locale è la lingua usata.

Ogni richiesta porta l’header X-Frontmail-Client: @frontmail/react-native/<version>.

Se il modello richiede Turnstile, mostra <TurnstileWebView> e passa il token a send. Non ti serve una chiave del sito: per impostazione predefinita il componente usa la chiave mobile condivisa di Frontmail. La recupera da GET /v1/public-config dell’API impostata nel provider (con cache, quindi viene caricata una volta sola) e mostra il widget di Cloudflare in react-native-webview come pagina HTML inline con l’URL https://mobile.frontmail.dev – da quell’indirizzo non viene caricato nulla.

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 });
// I token sono monouso – prendine uno nuovo per il tentativo successivo.
setToken(undefined);
turnstile.current?.reset();
};
return (
<>
{/* …campi… */}
<TurnstileWebView
ref={turnstile}
onToken={setToken}
onExpire={() => setToken(undefined)}
onError={(e) => console.warn(e.message)}
/>
<Button title="Invia" disabled={!token || status === 'sending'} onPress={onSubmit} />
</>
);
}

Props: onToken e, facoltativamente, siteKey, baseUrl, onError, onExpire, theme (auto | light | dark), size (normal | compact | flexible), action, language, style e webViewProps (passate alla WebView).

La WebView è vincolata al widget: carica solo la pagina inline (baseUrl) e la challenge di Cloudflare, i link all’interno del widget si aprono nel browser di sistema e solo i messaggi della pagina del widget arrivano alle tue callback. webViewProps non può sovrascrivere source, originWhitelist, il controllo della navigazione, onMessage né le impostazioni di JavaScript e di accesso ai file.

Le richieste dall’app non hanno l’header Origin, quindi Frontmail verifica i loro token con la sua chiave segreta mobile condivisa. Per questo non ti servono chiavi Turnstile tue – con Consenti app mobile attivo puoi attivare il CAPTCHA sui modelli dell’app anche se la tua organizzazione non ha ancora chiavi.

Per usare invece il tuo widget Turnstile, passa sia siteKey sia baseUrl:

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

L’SDK invia allora automaticamente i token di questo widget con turnstile_key: "org", e Frontmail li verifica con la chiave segreta impostata in Sicurezza → Protezione dai bot (Turnstile).

Un token è valido per circa 5 minuti e può essere usato una sola volta: chiama ref.current.reset() dopo ogni invio, riuscito o no.

limitRate limita gli invii sul dispositivo, ad es. al massimo un messaggio ogni 30 secondi. In React Native non esiste location.pathname, quindi imposta sempre 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),
};

Senza storageProvider, l’SDK usa AsyncStorage quando è installato; altrimenti tiene il limite solo in memoria (si azzera al riavvio dell’app) e in sviluppo stampa un avviso. Usa memoryStorageProvider() per scegliere esplicitamente l’archiviazione in memoria.

limitRate protegge solo dai tocchi ripetuti per errore – la vera protezione è sul server (vedi Sicurezza).

getStatus() dell’hook legge lo stato di recapito dell’ultimo messaggio accettato. Per tutto il resto, useFrontmail() restituisce il client del provider (send, getStatus):

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

Gli errori di rete, i 5xx e i 429 vengono ripetuti automaticamente, e tutti i tentativi di un invio riutilizzano la stessa chiave di idempotenza, quindi un nuovo tentativo non invia mai l’email due volte. Le chiavi sono UUID v4 – l’SDK usa crypto.randomUUID e, su Hermes, che non lo fornisce, ricorre a un proprio generatore. Vedi Tentativi e idempotenza.

Con i tipi generati (npx frontmail types estende @frontmail/react-native), useSendEmail('svc_…', 'tpl_contact').send({...}) controlla i tipi dei parametri di quel modello.

error è un FrontmailError con code, status e message. Come gestire ciascun codice è spiegato in Gestione degli errori.

  • In un’app metti sempre e solo la chiave pubblica (pk_…). Tutto ciò che sta nel bundle di un’app può essere estratto, quindi una chiave privata lì dentro è come se fosse pubblicata. Vedi Chiavi pubbliche e private.
  • Con Consenti app mobile attivo, le richieste senza header Origin vengono accettate da qualsiasi parte – qualunque script che conosca la tua chiave pubblica può inviarle. Affidati al rate limit per IP, a Turnstile su ogni modello usato dall’app e alla lista dei bloccati.

Progetto di esempio: examples/react-native.