chore: post-review cleanup (3 Important fixes + dead code purge)
Addresses the 3 Important issues + 4 follow-ups flagged by the final code review subagent. ## Important fixes - **getNextRef pagination** (cloudflare-worker/hubspot-proxy.js): previously used HubSpot search with limit:100 without pagination, causing reference-number collisions once contact count exceeded 100 (= search results don't guarantee ordering by ref). Now paginates through all results via paging.next.after cursor to find the true numeric maximum. ~10 API calls for 1000 contacts. - **Turnstile reset after Worker calls** (js/form-handler.js): Cloudflare Turnstile tokens are single-use server-side. Without explicit reset, a re-submit would 403 silently from siteverify. New helper resetTurnstile() clears window.turnstileToken and calls window.turnstile.reset() to force a fresh challenge. Called after both requestVerification and sendWelcomeBack flows. - **notifyDuplicateViaFormspree removed** (js/form-handler.js): was actively POSTing returning customers' name+email to Formspree (FORMSPREE_ID='mojrvokp' = real endpoint, sentinel guard never triggered). PII leak to a third-party service inconsistent with the new Resend-only architecture. Function + call site removed. ## Dead code purge - form-handler.js: removed generateRefNumber (= dead, no callers), submitToHubSpot, submitToFormspree, notifyDuplicateViaFormspree functions. Removed constants HUBSPOT_PORTAL_ID, HUBSPOT_FORM_GUID, FORMSPREE_ID, EMAILJS_PUBLIC_KEY, EMAILJS_SERVICE_ID, EMAILJS_TEMPLATE_ID, EMAILJS_TEMPLATE_WELCOME_BACK. Removed emailjs.init() block. Net -111 lines. - contact.html + confirmation.html: removed <script> tag loading EmailJS browser SDK from jsDelivr (~30KB gzipped per page). - cloudflare-worker/hubspot-proxy.js: removed unused HUBSPOT_PORTAL_ID + HUBSPOT_FORM_GUID constants. Removed listSubscriptions and subscribe action handlers (= 0 callers in frontend, debug-only, reachable by unauthenticated POST without Turnstile guard). - cloudflare-worker/DEPLOIEMENT.md: KV ID note updated to reflect the actual ID in wrangler.toml. ## Net diff -116 lines (= 92 added, 208 removed across 4 files). Refs: post-cutover polish, addresses code review 2026-05-07.
This commit is contained in:
parent
e14b0ff01a
commit
dd23a46603
@ -55,7 +55,7 @@ Dans **Paramètres → Stockage et bases de données → Bindings KV** :
|
|||||||
3. Namespace : **Créer** un nouveau namespace nommé `mva-welcome-tracker`
|
3. Namespace : **Créer** un nouveau namespace nommé `mva-welcome-tracker`
|
||||||
4. Sauvegarder
|
4. Sauvegarder
|
||||||
|
|
||||||
Si déployé via `wrangler` : mettre à jour `wrangler.toml` avec l'ID du namespace KV créé (remplacer `REPLACE_AT_DEPLOY_TIME`).
|
Si déployé via `wrangler` : `wrangler.toml` contient déjà l'ID du namespace KV (`c02656ba22064923ab1c6db06b0f4a56` sur le compte CF `sergemind4s@gmail.com`). Pour un autre compte, recréer le namespace via `wrangler kv namespace create WELCOME_KV` puis remplacer l'ID dans `wrangler.toml`.
|
||||||
|
|
||||||
### 4. Vérifier le scope HubSpot
|
### 4. Vérifier le scope HubSpot
|
||||||
|
|
||||||
|
|||||||
@ -35,8 +35,6 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
const HUBSPOT_API = 'https://api.hubapi.com';
|
const HUBSPOT_API = 'https://api.hubapi.com';
|
||||||
const HUBSPOT_PORTAL_ID = '148163754';
|
|
||||||
const HUBSPOT_FORM_GUID = '1d9b75c9-8b60-4966-aa18-4bf503452e9a';
|
|
||||||
|
|
||||||
const corsHeaders = {
|
const corsHeaders = {
|
||||||
'Access-Control-Allow-Origin' : '*',
|
'Access-Control-Allow-Origin' : '*',
|
||||||
@ -271,43 +269,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── action: listSubscriptions (debug : trouver les IDs) ──
|
|
||||||
if (action === 'listSubscriptions') {
|
|
||||||
// Endpoint legacy email/public/v1 nécessite scope content au lieu de
|
|
||||||
// communication_preferences (que notre token n'a pas)
|
|
||||||
const r = await fetch(`${HUBSPOT_API}/email/public/v1/subscriptions`, {
|
|
||||||
headers: { 'Authorization': `Bearer ${token}` },
|
|
||||||
});
|
|
||||||
return jsonResponse(await r.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── action: subscribe ────────────────────────────────────
|
|
||||||
// Inscrit un contact à un type d'abonnement marketing (déclenche
|
|
||||||
// l'envoi du mail de double opt-in si DOI activé au niveau compte).
|
|
||||||
if (action === 'subscribe') {
|
|
||||||
if (!email || typeof email !== 'string') {
|
|
||||||
return jsonResponse({ error: 'Email requis' }, 400);
|
|
||||||
}
|
|
||||||
const subId = body.subscriptionId;
|
|
||||||
if (!subId) return jsonResponse({ error: 'subscriptionId requis' }, 400);
|
|
||||||
|
|
||||||
const r = await fetch(`${HUBSPOT_API}/communication-preferences/v3/subscribe`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
emailAddress: email.toLowerCase().trim(),
|
|
||||||
subscriptionId: subId,
|
|
||||||
legalBasis: 'LEGITIMATE_INTEREST_CLIENT',
|
|
||||||
legalBasisExplanation: 'Soumission du formulaire MVA Global Fret',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const data = await r.text();
|
|
||||||
return jsonResponse({ status: r.status, body: data });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── action par défaut : vérification doublon par email ──
|
// ── action par défaut : vérification doublon par email ──
|
||||||
if (!email || typeof email !== 'string') {
|
if (!email || typeof email !== 'string') {
|
||||||
return jsonResponse({ error: 'Email requis' }, 400);
|
return jsonResponse({ error: 'Email requis' }, 400);
|
||||||
@ -348,25 +309,37 @@ async function searchContactByEmail(token, email) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getNextRef(token) {
|
async function getNextRef(token) {
|
||||||
|
// Paginate through ALL HubSpot contacts with `reference_client` property to
|
||||||
|
// find the true numeric maximum. Previous version used `limit: 100` without
|
||||||
|
// pagination — produced collisions once the contact count exceeded 100
|
||||||
|
// because HubSpot search results don't guarantee ordering by ref. With
|
||||||
|
// pagination, we walk the full set: 100 per page × N pages until no more.
|
||||||
|
// For ~1000 contacts = 10 API calls. Acceptable cost given that this runs
|
||||||
|
// once per signup confirmation (= rare path).
|
||||||
|
let maxNum = 0;
|
||||||
|
let after; // undefined on first iteration
|
||||||
|
do {
|
||||||
|
const body = {
|
||||||
|
filterGroups: [{
|
||||||
|
filters: [{ propertyName: 'reference_client', operator: 'HAS_PROPERTY' }],
|
||||||
|
}],
|
||||||
|
properties: ['reference_client'],
|
||||||
|
limit: 100,
|
||||||
|
};
|
||||||
|
if (after) body.after = after;
|
||||||
|
|
||||||
const res = await fetch(`${HUBSPOT_API}/crm/v3/objects/contacts/search`, {
|
const res = await fetch(`${HUBSPOT_API}/crm/v3/objects/contacts/search`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${token}`,
|
'Authorization': `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(body),
|
||||||
filterGroups: [{
|
|
||||||
filters: [{ propertyName: 'reference_client', operator: 'HAS_PROPERTY' }],
|
|
||||||
}],
|
|
||||||
properties: ['reference_client'],
|
|
||||||
limit: 100,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`HubSpot search failed: ${res.status}`);
|
throw new Error(`HubSpot search failed: ${res.status}`);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
let maxNum = 0;
|
|
||||||
(data.results || []).forEach(c => {
|
(data.results || []).forEach(c => {
|
||||||
const m = (c.properties?.reference_client || '').match(/^MVA-(\d+)$/);
|
const m = (c.properties?.reference_client || '').match(/^MVA-(\d+)$/);
|
||||||
if (m) {
|
if (m) {
|
||||||
@ -374,6 +347,8 @@ async function getNextRef(token) {
|
|||||||
if (n > maxNum) maxNum = n;
|
if (n > maxNum) maxNum = n;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
after = data.paging?.next?.after;
|
||||||
|
} while (after);
|
||||||
return 'MVA-' + String(maxNum + 1).padStart(3, '0');
|
return 'MVA-' + String(maxNum + 1).padStart(3, '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,9 +13,6 @@
|
|||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
|
||||||
<!-- EmailJS -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@emailjs/browser@4/dist/email.min.js"></script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.confirmation-shell {
|
.confirmation-shell {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|||||||
@ -305,7 +305,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@emailjs/browser@4/dist/email.min.js"></script>
|
|
||||||
<script src="js/translations.js"></script>
|
<script src="js/translations.js"></script>
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
||||||
<script src="js/form-handler.js"></script>
|
<script src="js/form-handler.js"></script>
|
||||||
|
|||||||
@ -1,33 +1,24 @@
|
|||||||
// ============================================
|
// ============================================
|
||||||
// MVA Global Fret — Form Handler
|
// MVA Global Fret — Form Handler
|
||||||
// HubSpot Portal ID : 148163754
|
// ============================================
|
||||||
// HubSpot Form GUID : 1d9b75c9-8b60-4966-aa18-4bf503452e9a
|
// Frontend logic for contact.html (= inscription form):
|
||||||
|
// - validate inputs + Cloudflare Turnstile token
|
||||||
|
// - call Cloudflare Worker mva-hubspot-proxy for HubSpot dedup +
|
||||||
|
// sending verification email (= action requestVerification) or
|
||||||
|
// "Ravis de vous revoir" email for returning customers (=
|
||||||
|
// action sendWelcomeBack)
|
||||||
|
// - reset Turnstile widget after each Worker call (= tokens are
|
||||||
|
// single-use server-side; without reset, a re-submit silently
|
||||||
|
// 403s from Cloudflare's siteverify endpoint)
|
||||||
|
//
|
||||||
|
// All HubSpot/Resend transactions go through the Worker. No direct
|
||||||
|
// EmailJS / Formspree / HubSpot Forms API calls from the browser.
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
const HUBSPOT_PORTAL_ID = '148163754';
|
// ── PROXY CLOUDFLARE WORKER ──────────────────────────────────────
|
||||||
const HUBSPOT_FORM_GUID = '1d9b75c9-8b60-4966-aa18-4bf503452e9a';
|
// Worker URL (= deployed via wrangler from cloudflare-worker/).
|
||||||
const FORMSPREE_ID = 'mojrvokp';
|
// CORS Access-Control-Allow-Origin: * so the browser can call it
|
||||||
|
// directly.
|
||||||
// ── 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.sergemind4s.workers.dev';
|
const WORKER_PROXY_URL = 'https://mva-hubspot-proxy.sergemind4s.workers.dev';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@ -35,33 +26,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (form) setupContactForm(form);
|
if (form) setupContactForm(form);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Génération séquentielle via le Worker HubSpot : MVA-001, MVA-002, etc.
|
// ── TURNSTILE TOKEN MANAGEMENT ───────────────────────────────────
|
||||||
// Fallback sur un timestamp court si le Worker est indisponible.
|
// Reset the Turnstile widget + global token after each Worker call.
|
||||||
async function generateRefNumber() {
|
// Cloudflare Turnstile tokens are single-use server-side: a token
|
||||||
if (WORKER_PROXY_URL) {
|
// already submitted to siteverify cannot be re-used. Without an
|
||||||
try {
|
// explicit reset, a re-submit (= same form, same widget) would send
|
||||||
const res = await fetch(WORKER_PROXY_URL, {
|
// the now-consumed token and Cloudflare would 403 silently.
|
||||||
method: 'POST',
|
function resetTurnstile() {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
window.turnstileToken = null;
|
||||||
body: JSON.stringify({ action: 'nextRef' }),
|
if (window.turnstile && typeof window.turnstile.reset === 'function') {
|
||||||
});
|
try { window.turnstile.reset(); } catch (_) { /* widget absent */ }
|
||||||
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.
|
// Vérifie si l'email existe déjà dans HubSpot via le proxy Worker.
|
||||||
// Retourne les propriétés du contact existant, ou null si nouveau client / proxy non configuré.
|
// Retourne les propriétés du contact existant, ou null si nouveau
|
||||||
|
// client / Worker indisponible.
|
||||||
async function checkExistingContact(email) {
|
async function checkExistingContact(email) {
|
||||||
// Si le proxy n'est pas encore déployé, on laisse passer sans bloquer
|
|
||||||
if (!WORKER_PROXY_URL) return null;
|
if (!WORKER_PROXY_URL) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(WORKER_PROXY_URL, {
|
const res = await fetch(WORKER_PROXY_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -72,7 +54,6 @@ async function checkExistingContact(email) {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return data.total > 0 ? data.results[0].properties : null;
|
return data.total > 0 ? data.results[0].properties : null;
|
||||||
} catch {
|
} catch {
|
||||||
// Erreur réseau ou Worker indisponible : on laisse passer
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -82,7 +63,7 @@ function setupContactForm(form) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!validateForm(form)) return;
|
if (!validateForm(form)) return;
|
||||||
|
|
||||||
// ── VÉRIFICATION TURNSTILE (CAPTCHA anti-bot) ────────────────────────────
|
// ── VÉRIFICATION TURNSTILE (CAPTCHA anti-bot) ────────────────
|
||||||
if (!window.turnstileToken) {
|
if (!window.turnstileToken) {
|
||||||
const errEl = document.getElementById('formErrorGlobal');
|
const errEl = document.getElementById('formErrorGlobal');
|
||||||
if (errEl) {
|
if (errEl) {
|
||||||
@ -91,23 +72,20 @@ function setupContactForm(form) {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const email = form.email.value.trim();
|
const email = form.email.value.trim();
|
||||||
|
|
||||||
// ── VÉRIFICATION DOUBLON ──────────────────────────────────────────────────
|
// ── VÉRIFICATION DOUBLON ─────────────────────────────────────
|
||||||
// Vérifie HubSpot. Comme les contacts ne sont créés QU'APRÈS confirmation
|
// Comme les contacts ne sont créés QU'APRÈS confirmation email,
|
||||||
// email, ce check ne retourne que les vrais clients déjà inscrits (pas
|
// ce check ne retourne que les vrais clients déjà inscrits.
|
||||||
// les inscriptions en attente de confirmation).
|
|
||||||
const existing = await checkExistingContact(email);
|
const existing = await checkExistingContact(email);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
showAlreadyRegistered(existing);
|
showAlreadyRegistered(existing);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
firstname: form.firstname.value.trim(),
|
firstname: form.firstname.value.trim(),
|
||||||
@ -117,10 +95,11 @@ function setupContactForm(form) {
|
|||||||
address: form.address.value.trim(),
|
address: form.address.value.trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── ENVOI VERS LE WORKER ──────────────────────────────────────────────────
|
// ── ENVOI VERS LE WORKER ─────────────────────────────────────
|
||||||
// Le Worker stocke les données en KV (24h), envoie un email de validation
|
// Le Worker stocke les données en KV (24h) et envoie un email de
|
||||||
// via Brevo. Le contact n'est créé dans HubSpot QUE quand l'utilisateur
|
// validation via Resend. Le contact n'est créé dans HubSpot QUE
|
||||||
// clique sur le lien de confirmation (anti-pollution du CRM).
|
// quand l'utilisateur clique sur le lien de confirmation
|
||||||
|
// (anti-pollution du CRM).
|
||||||
let ok = false;
|
let ok = false;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(WORKER_PROXY_URL, {
|
const res = await fetch(WORKER_PROXY_URL, {
|
||||||
@ -138,6 +117,8 @@ function setupContactForm(form) {
|
|||||||
console.warn('[requestVerification]', err);
|
console.warn('[requestVerification]', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset Turnstile after the Worker call (= regardless of result)
|
||||||
|
resetTurnstile();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
@ -148,73 +129,7 @@ function setupContactForm(form) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── SOUMISSION HUBSPOT ────────────────────────────────────────────────────────
|
// ── VALIDATION ───────────────────────────────────────────────────
|
||||||
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) {
|
function validateForm(form) {
|
||||||
let valid = true;
|
let valid = true;
|
||||||
const lang = localStorage.getItem('mva-lang') || 'fr';
|
const lang = localStorage.getItem('mva-lang') || 'fr';
|
||||||
@ -263,7 +178,7 @@ function isValidPhone(phone) {
|
|||||||
return /^[+\d][\d\s\-().]{6,20}$/.test(phone);
|
return /^[+\d][\d\s\-().]{6,20}$/.test(phone);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── AFFICHAGE ─────────────────────────────────────────────────────────────────
|
// ── AFFICHAGE ────────────────────────────────────────────────────
|
||||||
function showFieldError(name, msg) {
|
function showFieldError(name, msg) {
|
||||||
const el = document.getElementById(`error-${name}`);
|
const el = document.getElementById(`error-${name}`);
|
||||||
const input = document.getElementById(name) || document.querySelector(`[name="${name}"]`);
|
const input = document.getElementById(name) || document.querySelector(`[name="${name}"]`);
|
||||||
@ -296,9 +211,6 @@ function setLoading(isLoading) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showSuccess(_refNumber, _clientData) {
|
function showSuccess(_refNumber, _clientData) {
|
||||||
// L'envoi de l'email de validation est déjà fait dans setupContactForm
|
|
||||||
// via l'appel Worker requestVerification — on n'a plus rien à faire ici
|
|
||||||
// sauf afficher la confirmation à l'écran.
|
|
||||||
const successEl = document.getElementById('formSuccess');
|
const successEl = document.getElementById('formSuccess');
|
||||||
const form = document.getElementById('contactForm');
|
const form = document.getElementById('contactForm');
|
||||||
if (successEl) {
|
if (successEl) {
|
||||||
@ -308,11 +220,11 @@ function showSuccess(_refNumber, _clientData) {
|
|||||||
if (form) form.style.display = 'none';
|
if (form) form.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── EMAIL "RAVIS DE TE REVOIR" (client déjà inscrit) ──────────────────────────
|
// ── EMAIL "RAVIS DE VOUS REVOIR" (client déjà inscrit) ───────────
|
||||||
// Rappelle au client son numéro de référence existant — n'écrit RIEN dans HubSpot.
|
// Rappelle au client son numéro de référence existant — n'écrit
|
||||||
// Passe par le Cloudflare Worker (action: sendWelcomeBack) qui délègue à Resend
|
// RIEN dans HubSpot. Passe par le Cloudflare Worker (action:
|
||||||
// — footer 2026 cohérent avec les autres emails transactionnels. Anti-bot via
|
// sendWelcomeBack) qui délègue à Resend. Anti-bot via Turnstile :
|
||||||
// Turnstile : on transmet le token déjà validé au moment du submit du formulaire.
|
// transmet le token déjà validé au moment du submit du formulaire.
|
||||||
async function sendWelcomeBackEmail(contact) {
|
async function sendWelcomeBackEmail(contact) {
|
||||||
if (!WORKER_PROXY_URL) return;
|
if (!WORKER_PROXY_URL) return;
|
||||||
if (!contact || !contact.email) return;
|
if (!contact || !contact.email) return;
|
||||||
@ -329,13 +241,14 @@ async function sendWelcomeBackEmail(contact) {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Erreur réseau : on n'interrompt pas l'UX (le client voit déjà sa ref dans le UI)
|
// Erreur réseau : on n'interrompt pas l'UX (le client voit
|
||||||
|
// déjà sa référence dans le UI).
|
||||||
console.warn('Worker sendWelcomeBack failed:', err);
|
console.warn('Worker sendWelcomeBack failed:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Affiche le message "déjà client" — ne modifie AUCUNE donnée HubSpot
|
// Affiche le message "déjà client" — ne modifie AUCUNE donnée HubSpot
|
||||||
function showAlreadyRegistered(contact) {
|
async function showAlreadyRegistered(contact) {
|
||||||
const lang = localStorage.getItem('mva-lang') || 'fr';
|
const lang = localStorage.getItem('mva-lang') || 'fr';
|
||||||
const t = translations?.[lang]?.contact || {};
|
const t = translations?.[lang]?.contact || {};
|
||||||
|
|
||||||
@ -357,11 +270,11 @@ function showAlreadyRegistered(contact) {
|
|||||||
|
|
||||||
if (form) form.style.display = 'none';
|
if (form) form.style.display = 'none';
|
||||||
|
|
||||||
// Envoi d'une notification interne à MVA (sans modifier les données du client)
|
// Email "Ravis de vous revoir" via Worker + Resend (= footer 2026
|
||||||
notifyDuplicateViaFormspree(contact);
|
// cohérent avec les autres emails transactionnels).
|
||||||
|
await sendWelcomeBackEmail(contact);
|
||||||
// Email "Ravis de te revoir" via Worker + Resend (= footer 2026 cohérent)
|
// Reset Turnstile after the Worker call (= prevent reuse).
|
||||||
sendWelcomeBackEmail(contact);
|
resetTurnstile();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError() {
|
function showError() {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user