site-mva-global-fret/js/form-handler.js
MVA Global Fret 313c870ea4 Fix bugs inscription: ref dupliquée + email de bienvenue manquant
Bug 1 — Ref MVA-001 dupliquée :
Le filtre HubSpot 'HAS_PROPERTY' avec value:'' retournait 0 résultats.
Suppression du value:'' → maintenant le worker liste correctement les
contacts avec reference_client et incrémente bien (testé : MVA-004).

Bug 2 — Email post-inscription jamais reçu :
Le double opt-in HubSpot ne se déclenche pas via Forms API sans
subscription consent (impossible à configurer sans nouveaux scopes
Private App). Pivot vers une approche plus simple :
- L'email de bienvenue est désormais envoyé directement après
  soumission du formulaire (pas de DOI HubSpot)
- L'envoi passe par le Cloudflare Worker (action sendWelcomeNow)
  pour que l'adresse Paris reste dans les env vars Cloudflare et
  ne soit JAMAIS dans le JS public
- Worker appelle EmailJS REST avec firstname + reference + paris_address

Cleanup : message de succès reverti à 'Inscription réussie' (FR/EN/MG).

Anti-spam : protection légère via filtre email/téléphone côté formulaire.
La cron-based welcome (post-DOI) reste en place mais sera inerte tant
que aucun contact n'a le statut CONFIRMED côté HubSpot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 09:52:48 +02:00

373 lines
14 KiB
JavaScript

// ============================================
// MVA Global Fret — Form Handler
// HubSpot Portal ID : 148163754
// HubSpot Form GUID : 1d9b75c9-8b60-4966-aa18-4bf503452e9a
// ============================================
const HUBSPOT_PORTAL_ID = '148163754';
const HUBSPOT_FORM_GUID = '1d9b75c9-8b60-4966-aa18-4bf503452e9a';
const FORMSPREE_ID = 'mojrvokp';
// ── EMAILJS (email de bienvenue au client) ────────────────────────────────────
const EMAILJS_PUBLIC_KEY = '8KUlaQ7BDVIbkZRyP';
const EMAILJS_SERVICE_ID = 'service_aeamo3x';
const EMAILJS_TEMPLATE_ID = 'template_s1kr2et';
// Template pour les clients déjà inscrits ("Ravis de te revoir")
// ⚠️ À créer dans EmailJS puis remplacer la valeur ci-dessous
const EMAILJS_TEMPLATE_WELCOME_BACK = 'template_welcome_back';
// Initialisation EmailJS (une seule fois au chargement)
if (typeof emailjs !== 'undefined') {
emailjs.init({ publicKey: EMAILJS_PUBLIC_KEY });
}
// ── PROXY CLOUDFLARE WORKER ───────────────────────────────────────────────────
// URL du Worker qui proxifie l'API HubSpot CRM (contourne le CORS).
// Après déploiement du Worker (voir cloudflare-worker/hubspot-proxy.js),
// remplacer la chaîne vide par l'URL obtenue, ex :
// 'https://mva-hubspot-proxy.moncompte.workers.dev'
// Tant que cette constante est vide, la vérification doublon est désactivée
// (le formulaire s'envoie normalement — aucun blocage).
const WORKER_PROXY_URL = 'https://mva-hubspot-proxy.mvaglobalfret.workers.dev';
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('contactForm');
if (form) setupContactForm(form);
});
// Génération séquentielle via le Worker HubSpot : MVA-001, MVA-002, etc.
// Fallback sur un timestamp court si le Worker est indisponible.
async function generateRefNumber() {
if (WORKER_PROXY_URL) {
try {
const res = await fetch(WORKER_PROXY_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'nextRef' }),
});
if (res.ok) {
const data = await res.json();
if (data.nextRef) return data.nextRef;
}
} catch { /* fallback ci-dessous */ }
}
// Fallback : numéro aléatoire court pour éviter les doublons en cas d'indisponibilité
const rand = String(Math.floor(Math.random() * 900) + 100);
return `MVA-F${rand}`;
}
// Vérifie si l'email existe déjà dans HubSpot via le proxy Cloudflare Worker.
// Retourne les propriétés du contact existant, ou null si nouveau client / proxy non configuré.
async function checkExistingContact(email) {
// Si le proxy n'est pas encore déployé, on laisse passer sans bloquer
if (!WORKER_PROXY_URL) return null;
try {
const res = await fetch(WORKER_PROXY_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email.toLowerCase().trim() }),
});
if (!res.ok) return null;
const data = await res.json();
return data.total > 0 ? data.results[0].properties : null;
} catch {
// Erreur réseau ou Worker indisponible : on laisse passer
return null;
}
}
function setupContactForm(form) {
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!validateForm(form)) return;
setLoading(true);
const email = form.email.value.trim();
// ── VÉRIFICATION DOUBLON ──────────────────────────────────────────────────
// Si le client existe déjà, on affiche uniquement un message informatif.
// On ne soumet RIEN à HubSpot — la référence existante n'est JAMAIS modifiée.
const existing = await checkExistingContact(email);
if (existing) {
setLoading(false);
showAlreadyRegistered(existing);
return; // ← arrêt complet, aucune soumission
}
// ─────────────────────────────────────────────────────────────────────────
// Nouveau client : génération du numéro de référence unique
const refNumber = await generateRefNumber();
const data = {
firstname: form.firstname.value.trim(),
lastname: form.lastname.value.trim(),
phone: form.phone.value.trim(),
email: email,
address: form.address.value.trim(),
reference_client: refNumber,
};
const results = await Promise.allSettled([
submitToHubSpot(data),
submitToFormspree(data),
]);
const hubspotOk = results[0].status === 'fulfilled';
const formspreeOk = results[1].status === 'fulfilled';
setLoading(false);
if (hubspotOk || formspreeOk) {
showSuccess(refNumber, data);
} else {
showError();
}
});
}
// ── SOUMISSION HUBSPOT ────────────────────────────────────────────────────────
async function submitToHubSpot(data) {
const payload = {
fields: [
{ name: 'firstname', value: data.firstname },
{ name: 'lastname', value: data.lastname },
{ name: 'phone', value: data.phone },
{ name: 'email', value: data.email },
{ name: 'address', value: data.address },
{ name: 'reference_client', value: data.reference_client },
],
context: {
pageUri: window.location.href,
pageName: document.title,
},
};
const res = await fetch(
`https://api.hsforms.com/submissions/v3/integration/submit/${HUBSPOT_PORTAL_ID}/${HUBSPOT_FORM_GUID}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}
);
if (!res.ok) throw new Error(`HubSpot error: ${res.status}`);
return res.json();
}
// ── SOUMISSION FORMSPREE (email de backup) ────────────────────────────────────
async function submitToFormspree(data) {
if (FORMSPREE_ID === 'YOUR_FORMSPREE_ID') return;
const res = await fetch(`https://formspree.io/f/${FORMSPREE_ID}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
nom: data.lastname,
prenom: data.firstname,
telephone: data.phone,
email: data.email,
adresse_livraison: data.address,
reference_client: data.reference_client,
}),
});
if (!res.ok) throw new Error(`Formspree error: ${res.status}`);
return res.json();
}
// ── NOTIFICATION DOUBLON (email interne seulement, sans toucher aux données) ──
async function notifyDuplicateViaFormspree(contact) {
if (FORMSPREE_ID === 'YOUR_FORMSPREE_ID') return;
try {
await fetch(`https://formspree.io/f/${FORMSPREE_ID}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
_subject: `[MVA] Tentative double inscription — ${contact.firstname || ''} ${contact.lastname || ''}`,
message: `Le client ${contact.firstname || ''} ${contact.lastname || ''} (${contact.email}) a tenté de s'inscrire à nouveau. Référence existante : ${contact.reference_client || 'non définie'}.`,
}),
});
} catch { /* Ne pas bloquer l'interface si la notification échoue */ }
}
// ── 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';
// Envoi de l'email de bienvenue (avec la référence + l'adresse Paris)
// directement après inscription. Les bots qui soumettent avec un faux
// email ne reçoivent rien (boîte inexistante = bounce). Pour bloquer
// les bots qui utilisent de vrais emails, voir la protection honeypot
// dans validateForm() + le rate limit côté worker.
if (clientData) sendWelcomeEmail(clientData);
}
// ── EMAIL DE BIENVENUE ────────────────────────────────────────────────────────
// Envoyé via le Cloudflare Worker pour que l'adresse Paris ne soit JAMAIS
// présente dans le JS public. Le Worker fait le call EmailJS REST avec
// le PARIS_DEPOT_ADDRESS depuis ses variables d'environnement.
async function sendWelcomeEmail(data) {
if (!WORKER_PROXY_URL) return;
try {
await fetch(WORKER_PROXY_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'sendWelcomeNow',
firstname: data.firstname,
email: data.email,
reference_client: data.reference_client,
}),
});
} catch (err) {
// L'email de bienvenue est un bonus — on ne bloque pas l'inscription si ça échoue
console.warn('Welcome email failed:', err);
}
}
// ── EMAIL "RAVIS DE TE REVOIR" (client déjà inscrit) ──────────────────────────
// Rappelle au client son numéro de référence existant — n'écrit RIEN dans HubSpot.
async function sendWelcomeBackEmail(contact) {
if (typeof emailjs === 'undefined') return;
if (!contact || !contact.email) return;
try {
await emailjs.send(EMAILJS_SERVICE_ID, EMAILJS_TEMPLATE_WELCOME_BACK, {
firstname: contact.firstname || '',
email: contact.email,
reference_client: contact.reference_client || '',
});
} catch (err) {
// Si le template n'existe pas encore ou erreur réseau : on n'interrompt rien
console.warn('EmailJS welcome-back email failed:', err);
}
}
// Affiche le message "déjà client" — ne modifie AUCUNE donnée HubSpot
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';
// Envoi d'une notification interne à MVA (sans modifier les données du client)
notifyDuplicateViaFormspree(contact);
// Envoi d'un email "Ravis de te revoir" au client avec son n° de référence
sendWelcomeBackEmail(contact);
}
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.';
}
}