La page declarait des sous-traitants faux/obsoletes (risque de conformite RGPD): retire HubSpot (decommissionne 2026-05-10) + son transfert hors-UE; Brevo -> Resend (USA) pour les emails; ajoute Cloudflare/Turnstile (USA, traite l'IP du visiteur, sous-traitant non declare); precise que la base de donnees (inscriptions/contacts) est hebergee par Hostinger (Allemagne, UE); corrige l'Article 6 transferts hors-UE en consequence. Applique aux 3 langues FR/EN/MG. Nettoie aussi les commentaires JS obsoletes (worker mva-hubspot-proxy decommissionne). Verifs: 0 mention HubSpot/Brevo restante, node --check OK sur form-handler.js + confirmation.js. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
286 lines
10 KiB
JavaScript
286 lines
10 KiB
JavaScript
// ============================================
|
|
// MVA Global Fret — Form Handler
|
|
// ============================================
|
|
// Frontend logic for contact.html (= inscription form):
|
|
// - validate inputs + Cloudflare Turnstile token
|
|
// - call mva-api /leads/* routes for dedup check + double opt-in flow
|
|
// (= verification email + welcome / welcome-back emails via Resend)
|
|
// - reset Turnstile widget after each API call (= tokens are
|
|
// single-use server-side; without reset, a re-submit silently
|
|
// 403s from Cloudflare's siteverify endpoint)
|
|
//
|
|
// Les inscriptions sont gérées par les routes leads de l'API (derrière Caddy)
|
|
// avec stockage en base Postgres.
|
|
// ============================================
|
|
|
|
// ── MVA API BASE URL ─────────────────────────────────────────────
|
|
// Routes leads servies par mva-api derrière Caddy. CORS strict :
|
|
// le serveur whitelist explicitement https://mva-globalfret.com.
|
|
const API_BASE_URL = 'https://api.mva.mind4solutions.com';
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const form = document.getElementById('contactForm');
|
|
if (form) setupContactForm(form);
|
|
});
|
|
|
|
// ── TURNSTILE TOKEN MANAGEMENT ───────────────────────────────────
|
|
// Reset the Turnstile widget + global token after each Worker call.
|
|
// Cloudflare Turnstile tokens are single-use server-side: a token
|
|
// already submitted to siteverify cannot be re-used. Without an
|
|
// explicit reset, a re-submit (= same form, same widget) would send
|
|
// the now-consumed token and Cloudflare would 403 silently.
|
|
function resetTurnstile() {
|
|
window.turnstileToken = null;
|
|
if (window.turnstile && typeof window.turnstile.reset === 'function') {
|
|
try { window.turnstile.reset(); } catch (_) { /* widget absent */ }
|
|
}
|
|
}
|
|
|
|
// Vérifie si l'email existe déjà dans la table leads via mva-api.
|
|
// Retourne les propriétés du lead existant, ou null si nouveau
|
|
// client / API indisponible.
|
|
async function checkExistingContact(email) {
|
|
try {
|
|
const res = await fetch(`${API_BASE_URL}/leads/check-email`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email: email.toLowerCase().trim() }),
|
|
});
|
|
if (!res.ok) return null;
|
|
const data = await res.json();
|
|
if (!data.exists) return null;
|
|
// Forme attendue par showAlreadyRegistered : { firstname, reference_client }
|
|
return {
|
|
firstname: data.firstname || '',
|
|
reference_client: data.reference_client || '',
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function setupContactForm(form) {
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
if (!validateForm(form)) return;
|
|
|
|
// ── VÉRIFICATION TURNSTILE (CAPTCHA anti-bot) ────────────────
|
|
if (!window.turnstileToken) {
|
|
const errEl = document.getElementById('formErrorGlobal');
|
|
if (errEl) {
|
|
errEl.style.display = 'block';
|
|
errEl.textContent = '🤖 Veuillez compléter le contrôle anti-robot ci-dessus avant d\'envoyer le formulaire.';
|
|
}
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
const email = form.email.value.trim();
|
|
|
|
// ── VÉRIFICATION DOUBLON ─────────────────────────────────────
|
|
// Comme les contacts ne sont créés QU'APRÈS confirmation email,
|
|
// ce check ne retourne que les vrais clients déjà inscrits.
|
|
const existing = await checkExistingContact(email);
|
|
if (existing) {
|
|
setLoading(false);
|
|
showAlreadyRegistered(existing);
|
|
return;
|
|
}
|
|
|
|
const data = {
|
|
firstname: form.firstname.value.trim(),
|
|
lastname: form.lastname.value.trim(),
|
|
phone: form.phone.value.trim(),
|
|
email: email,
|
|
address: form.address.value.trim(),
|
|
};
|
|
|
|
// ── ENVOI VERS MVA-API ────────────────────────────────────────
|
|
// L'API stocke les données en `leads_pending` (24h TTL) et envoie un
|
|
// email de validation via Resend. Le lead n'est INSERT en `leads` QUE
|
|
// quand l'utilisateur clique sur le lien de confirmation
|
|
// (anti-pollution DB + anti-bot complémentaire à Turnstile).
|
|
let ok = false;
|
|
try {
|
|
const res = await fetch(`${API_BASE_URL}/leads/request-verification`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
...data,
|
|
turnstile_token: window.turnstileToken || '',
|
|
}),
|
|
});
|
|
const result = await res.json().catch(() => ({}));
|
|
ok = res.ok && result.ok === true;
|
|
} catch (err) {
|
|
console.warn('[requestVerification]', err);
|
|
}
|
|
|
|
// Reset Turnstile after the Worker call (= regardless of result)
|
|
resetTurnstile();
|
|
setLoading(false);
|
|
|
|
if (ok) {
|
|
showSuccess(null, data);
|
|
} else {
|
|
showError();
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── VALIDATION ───────────────────────────────────────────────────
|
|
function validateForm(form) {
|
|
let valid = true;
|
|
const lang = localStorage.getItem('mva-lang') || 'fr';
|
|
const t = translations?.[lang]?.contact || {};
|
|
|
|
const requiredMsg = t.required || 'Ce champ est obligatoire';
|
|
const invalidEmail = t.invalidEmail || 'Adresse email invalide';
|
|
const invalidPhone = t.invalidPhone || 'Numéro de téléphone invalide';
|
|
|
|
const fields = ['firstname', 'lastname', 'phone', 'email', 'address'];
|
|
fields.forEach(name => clearError(name));
|
|
clearError('cgv');
|
|
|
|
fields.forEach(name => {
|
|
const el = form[name];
|
|
if (!el.value.trim()) {
|
|
showFieldError(name, requiredMsg);
|
|
valid = false;
|
|
}
|
|
});
|
|
|
|
const cgvBox = form['cgv'];
|
|
if (cgvBox && !cgvBox.checked) {
|
|
showFieldError('cgv', t.cgvRequired || 'Vous devez accepter les Conditions Générales de Vente.');
|
|
valid = false;
|
|
}
|
|
|
|
if (form.email.value.trim() && !isValidEmail(form.email.value.trim())) {
|
|
showFieldError('email', invalidEmail);
|
|
valid = false;
|
|
}
|
|
|
|
if (form.phone.value.trim() && !isValidPhone(form.phone.value.trim())) {
|
|
showFieldError('phone', invalidPhone);
|
|
valid = false;
|
|
}
|
|
|
|
return valid;
|
|
}
|
|
|
|
function isValidEmail(email) {
|
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
}
|
|
|
|
function isValidPhone(phone) {
|
|
return /^[+\d][\d\s\-().]{6,20}$/.test(phone);
|
|
}
|
|
|
|
// ── AFFICHAGE ────────────────────────────────────────────────────
|
|
function showFieldError(name, msg) {
|
|
const el = document.getElementById(`error-${name}`);
|
|
const input = document.getElementById(name) || document.querySelector(`[name="${name}"]`);
|
|
if (el) el.textContent = msg;
|
|
if (input) input.classList.add('error');
|
|
}
|
|
|
|
function clearError(name) {
|
|
const el = document.getElementById(`error-${name}`);
|
|
const input = document.getElementById(name) || document.querySelector(`[name="${name}"]`);
|
|
if (el) el.textContent = '';
|
|
if (input) input.classList.remove('error');
|
|
}
|
|
|
|
function setLoading(isLoading) {
|
|
const btn = document.getElementById('submitBtn');
|
|
const txt = document.getElementById('submitText');
|
|
const form = document.getElementById('contactForm');
|
|
const lang = localStorage.getItem('mva-lang') || 'fr';
|
|
const t = translations?.[lang]?.contact || {};
|
|
|
|
if (!btn) return;
|
|
btn.disabled = isLoading;
|
|
if (txt) {
|
|
txt.textContent = isLoading
|
|
? (t.submitLoading || 'Envoi en cours...')
|
|
: (t.submitBtn || "S'inscrire");
|
|
}
|
|
form?.classList.toggle('form-loading', isLoading);
|
|
}
|
|
|
|
function showSuccess(_refNumber, _clientData) {
|
|
const successEl = document.getElementById('formSuccess');
|
|
const form = document.getElementById('contactForm');
|
|
if (successEl) {
|
|
successEl.classList.add('show');
|
|
successEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
if (form) form.style.display = 'none';
|
|
}
|
|
|
|
// ── EMAIL "RAVIS DE VOUS REVOIR" (client déjà inscrit) ───────────
|
|
// Rappelle au client son numéro de référence existant — zéro write DB.
|
|
// Passe par mva-api /leads/welcome-back qui délègue à Resend.
|
|
// Anti-bot via Turnstile : transmet le token déjà validé au moment du
|
|
// submit du formulaire.
|
|
async function sendWelcomeBackEmail(contact) {
|
|
if (!contact || !contact.email) return;
|
|
if (!window.turnstileToken) return;
|
|
try {
|
|
await fetch(`${API_BASE_URL}/leads/welcome-back`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
email : contact.email,
|
|
turnstile_token : window.turnstileToken,
|
|
}),
|
|
});
|
|
} catch (err) {
|
|
// Erreur réseau : on n'interrompt pas l'UX (le client voit
|
|
// déjà sa référence dans le UI).
|
|
console.warn('welcome-back failed:', err);
|
|
}
|
|
}
|
|
|
|
// Affiche le message "déjà client" — ne crée aucune nouvelle inscription
|
|
async function showAlreadyRegistered(contact) {
|
|
const lang = localStorage.getItem('mva-lang') || 'fr';
|
|
const t = translations?.[lang]?.contact || {};
|
|
|
|
const alreadyEl = document.getElementById('alreadyRegistered');
|
|
const form = document.getElementById('contactForm');
|
|
const refDisplay = document.getElementById('existingRefDisplay');
|
|
|
|
// Affiche la référence existante si disponible, sinon message de contact
|
|
if (refDisplay) {
|
|
refDisplay.textContent = contact.reference_client
|
|
? contact.reference_client
|
|
: (t.alreadyRefUnknown || 'Contactez-nous pour retrouver votre référence.');
|
|
}
|
|
|
|
if (alreadyEl) {
|
|
alreadyEl.classList.add('show');
|
|
alreadyEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
|
|
if (form) form.style.display = 'none';
|
|
|
|
// Email "Ravis de vous revoir" via Worker + Resend (= footer 2026
|
|
// cohérent avec les autres emails transactionnels).
|
|
await sendWelcomeBackEmail(contact);
|
|
// Reset Turnstile after the Worker call (= prevent reuse).
|
|
resetTurnstile();
|
|
}
|
|
|
|
function showError() {
|
|
const errEl = document.getElementById('formErrorGlobal');
|
|
const lang = localStorage.getItem('mva-lang') || 'fr';
|
|
const t = translations?.[lang]?.contact || {};
|
|
if (errEl) {
|
|
errEl.style.display = 'block';
|
|
errEl.textContent = t.errorMsg || 'Une erreur est survenue. Veuillez réessayer ou nous contacter directement.';
|
|
}
|
|
}
|