Add post-confirmation page that triggers welcome email after double opt-in

Flux complet du double opt-in :
1. User soumet le formulaire → contact créé en HubSpot avec sa référence
2. HubSpot envoie un email 'Confirmez votre inscription'
3. User clique 'Confirmer' → HubSpot le marque 'subscribed'
4. HubSpot redirige vers confirmation.html?email=...
5. La page lit l'email, appelle le Worker Cloudflare pour récupérer la
   référence du contact, et déclenche l'envoi de l'email de bienvenue
   via EmailJS (avec la référence dedans)
6. Affiche succès + référence à l'écran

Idempotence via localStorage pour éviter de spammer l'email à chaque
rechargement de la page.

À configurer dans HubSpot Settings > Marketing > Email > Confirmation
d'inscription : URL de redirection après confirmation =
https://mva-global-fret.github.io/site-mva-global-fret/confirmation.html?email={{contact.email}}

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MVA Global Fret 2026-05-05 23:03:19 +02:00
parent c713d40946
commit f534376f90
3 changed files with 335 additions and 0 deletions

180
confirmation.html Normal file
View File

@ -0,0 +1,180 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inscription confirmée — MVA Global Fret</title>
<meta name="description" content="Votre inscription chez MVA Global Fret est confirmée. Votre numéro de référence client vient d'être envoyé par email.">
<meta name="robots" content="noindex, nofollow">
<link rel="icon" type="image/png" href="PNG MVA GLOBAL FRET.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Poppins:wght@600;700&display=swap" rel="stylesheet">
<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">
<!-- EmailJS -->
<script src="https://cdn.jsdelivr.net/npm/@emailjs/browser@4/dist/email.min.js"></script>
<style>
.confirmation-shell {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.confirmation-main {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 80px 24px 40px;
background: linear-gradient(135deg, var(--navy) 0%, #2a2a5e 100%);
}
.confirmation-card {
max-width: 560px;
width: 100%;
background: var(--white);
border-radius: 20px;
padding: 48px 36px;
text-align: center;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.25);
border: 2px solid var(--gold);
}
.confirmation-card .icon-circle {
width: 88px;
height: 88px;
margin: 0 auto 24px;
border-radius: 50%;
background: linear-gradient(135deg, var(--gold), #e0c98a);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12px 30px rgba(197, 165, 90, 0.45);
}
.confirmation-card .icon-circle i {
font-size: 2.4rem;
color: var(--navy);
}
.confirmation-card h1 {
color: var(--navy);
margin-bottom: 16px;
font-size: clamp(1.6rem, 4vw, 2.2rem);
}
.confirmation-card p {
color: var(--text-light);
margin-bottom: 14px;
line-height: 1.6;
}
.confirmation-card .ref-block {
margin-top: 28px;
padding: 18px 24px;
background: var(--navy);
border-radius: 12px;
border: 2px solid var(--gold);
text-align: center;
}
.confirmation-card .ref-block small {
display: block;
color: var(--gold);
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 1px;
margin-bottom: 6px;
}
.confirmation-card .ref-block strong {
color: var(--white);
font-size: 1.6rem;
font-weight: 700;
letter-spacing: 2px;
}
.confirmation-card .actions {
margin-top: 32px;
display: flex;
gap: 12px;
flex-wrap: wrap;
justify-content: center;
}
.confirmation-card .actions .btn {
flex: 1 1 200px;
}
.confirmation-card .loader {
display: inline-block;
width: 18px;
height: 18px;
border: 2px solid rgba(26,26,62,0.2);
border-top-color: var(--navy);
border-radius: 50%;
animation: spin 0.7s linear infinite;
vertical-align: middle;
margin-right: 8px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.confirmation-card.is-error .icon-circle {
background: linear-gradient(135deg, #ff5a5a, #ff8a8a);
box-shadow: 0 12px 30px rgba(255, 90, 90, 0.45);
}
.confirmation-card.is-error .icon-circle i { color: var(--white); }
.confirmation-card.is-error { border-color: #ff5a5a; }
</style>
</head>
<body class="confirmation-shell">
<header class="header" id="header" style="background: rgba(26,26,62,0.95);">
<div class="container header-inner">
<a href="accueil.html" class="logo"><img src="PNG MVA GLOBAL FRET.png" alt="MVA Global Fret"></a>
<div class="header-right">
<div class="lang-switcher" role="group" aria-label="Choisir la langue">
<button data-lang="fr" class="active">FR</button>
<button data-lang="en">EN</button>
<button data-lang="mg">MG</button>
</div>
</div>
</div>
</header>
<main class="confirmation-main">
<!-- État : chargement (par défaut) -->
<div class="confirmation-card" id="cardLoading">
<div class="icon-circle"><i class="fa-solid fa-envelope-open-text"></i></div>
<h1 data-i18n="confirmation.loadingTitle">Confirmation en cours…</h1>
<p data-i18n="confirmation.loadingDesc">Nous finalisons votre inscription et préparons votre numéro de référence client.</p>
<p style="margin-top:18px;"><span class="loader"></span><span data-i18n="confirmation.loadingHint">Quelques secondes…</span></p>
</div>
<!-- État : succès -->
<div class="confirmation-card" id="cardSuccess" style="display:none;">
<div class="icon-circle"><i class="fa-solid fa-circle-check"></i></div>
<h1 data-i18n="confirmation.successTitle">Inscription confirmée !</h1>
<p data-i18n="confirmation.successDesc">Bienvenue chez MVA Global Fret. Votre numéro de référence client vous a été envoyé par email.</p>
<div class="ref-block" id="refBlock" style="display:none;">
<small data-i18n="confirmation.refLabel">VOTRE NUMÉRO DE RÉFÉRENCE CLIENT</small>
<strong id="refDisplay"></strong>
</div>
<p style="margin-top:18px; font-size:0.9rem;" data-i18n="confirmation.successHint">📧 Pensez aussi à vérifier vos spams.</p>
<div class="actions">
<a href="accueil.html" class="btn btn-primary" data-i18n="confirmation.backHome">Retour à l'accueil</a>
<a href="guide-envoi.html" class="btn btn-secondary" data-i18n="confirmation.guide">Guide d'envoi</a>
</div>
</div>
<!-- État : erreur -->
<div class="confirmation-card is-error" id="cardError" style="display:none;">
<div class="icon-circle"><i class="fa-solid fa-circle-exclamation"></i></div>
<h1 data-i18n="confirmation.errorTitle">Confirmation reçue</h1>
<p data-i18n="confirmation.errorDesc">Votre confirmation est bien enregistrée chez MVA Global Fret. En cas de question sur votre numéro de référence, contactez-nous directement.</p>
<div class="actions">
<a href="contact.html" class="btn btn-primary" data-i18n="confirmation.contactUs">Nous contacter</a>
<a href="accueil.html" class="btn btn-secondary" data-i18n="confirmation.backHome">Retour à l'accueil</a>
</div>
</div>
</main>
<footer style="background: var(--navy-dark, #050518); color: rgba(255,255,255,0.6); padding: 18px 24px; text-align:center; font-size: 0.85rem;">
© 2026 MVA Global Fret. Tous droits réservés.
</footer>
<script src="js/translations.js"></script>
<script src="js/main.js"></script>
<script src="js/confirmation.js"></script>
</body>
</html>

110
js/confirmation.js Normal file
View File

@ -0,0 +1,110 @@
// ============================================================
// MVA Global Fret — Page de confirmation post-double-opt-in
// ============================================================
// Cette page est la cible de redirection après que l'utilisateur
// a cliqué sur "Confirmer" dans l'email de validation HubSpot.
//
// HubSpot redirige vers :
// https://mva-global-fret.github.io/site-mva-global-fret/confirmation.html?email={contact.email}
//
// Étapes :
// 1. Lire l'email depuis l'URL
// 2. Demander au Cloudflare Worker la fiche du contact (incluant reference_client)
// 3. Envoyer un email de bienvenue via EmailJS contenant la référence
// 4. Afficher la référence à l'écran
//
// Si une étape échoue, on affiche un fallback poli (l'inscription
// reste valide côté HubSpot, c'est juste l'email qui ne part pas).
// ============================================================
const WORKER_PROXY_URL = 'https://mva-hubspot-proxy.mvaglobalfret.workers.dev';
const EMAILJS_PUBLIC_KEY = '8KUlaQ7BDVIbkZRyP';
const EMAILJS_SERVICE_ID = 'service_aeamo3x';
const EMAILJS_TEMPLATE_ID = 'template_s1kr2et';
// Marqueur localStorage : empêche de relancer l'envoi si l'utilisateur
// recharge la page après confirmation (par sécurité ET pour ne pas
// renvoyer 3 fois le même email).
const STORAGE_KEY_PREFIX = 'mva-confirm-sent-';
document.addEventListener('DOMContentLoaded', async () => {
if (typeof emailjs !== 'undefined') {
emailjs.init({ publicKey: EMAILJS_PUBLIC_KEY });
}
const email = getEmailFromUrl();
if (!email) {
// Pas d'email dans l'URL : on affiche quand même un succès générique
// (l'utilisateur a bien confirmé puisqu'il atterrit ici depuis HubSpot)
showSuccess(null);
return;
}
try {
const contact = await fetchContact(email);
if (!contact) {
// Contact non trouvé via Worker — on affiche succès quand même
showSuccess(null);
return;
}
const ref = contact.reference_client || null;
// Idempotence : un seul email par confirmation
const storageKey = STORAGE_KEY_PREFIX + email.toLowerCase();
if (!localStorage.getItem(storageKey)) {
await sendWelcomeEmail({
firstname: contact.firstname || '',
email: email,
reference_client: ref || '',
});
localStorage.setItem(storageKey, String(Date.now()));
}
showSuccess(ref);
} catch (err) {
console.warn('[confirmation] flow failed:', err);
showError();
}
});
function getEmailFromUrl() {
const params = new URLSearchParams(window.location.search);
const raw = params.get('email');
if (!raw) return null;
const email = raw.trim().toLowerCase();
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? email : null;
}
async function fetchContact(email) {
const res = await fetch(WORKER_PROXY_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
if (!res.ok) throw new Error('Worker error: ' + res.status);
const data = await res.json();
if (!data.results || data.results.length === 0) return null;
return data.results[0].properties;
}
async function sendWelcomeEmail(payload) {
if (typeof emailjs === 'undefined') return;
await emailjs.send(EMAILJS_SERVICE_ID, EMAILJS_TEMPLATE_ID, payload);
}
function showSuccess(ref) {
document.getElementById('cardLoading').style.display = 'none';
document.getElementById('cardSuccess').style.display = '';
if (ref) {
document.getElementById('refDisplay').textContent = ref;
document.getElementById('refBlock').style.display = '';
}
}
function showError() {
document.getElementById('cardLoading').style.display = 'none';
document.getElementById('cardError').style.display = '';
}

View File

@ -333,6 +333,21 @@ const translations = {
ctaSubtitle: "Contactez-nous avec votre lien produit, nous vous établissons un devis sous 24h.", ctaSubtitle: "Contactez-nous avec votre lien produit, nous vous établissons un devis sous 24h.",
ctaContact: "Nous contacter", ctaContact: "Nous contacter",
ctaMessenger: "Messenger" ctaMessenger: "Messenger"
},
confirmation: {
loadingTitle: "Confirmation en cours…",
loadingDesc: "Nous finalisons votre inscription et préparons votre numéro de référence client.",
loadingHint: "Quelques secondes…",
successTitle: "Inscription confirmée !",
successDesc: "Bienvenue chez MVA Global Fret. Votre numéro de référence client vous a été envoyé par email.",
successHint: "📧 Pensez aussi à vérifier vos spams.",
refLabel: "VOTRE NUMÉRO DE RÉFÉRENCE CLIENT",
errorTitle: "Confirmation reçue",
errorDesc: "Votre confirmation est bien enregistrée chez MVA Global Fret. En cas de question sur votre numéro de référence, contactez-nous directement.",
backHome: "Retour à l'accueil",
guide: "Guide d'envoi",
contactUs: "Nous contacter"
} }
}, },
@ -670,6 +685,21 @@ const translations = {
ctaSubtitle: "Contact us with your product link, we'll send you a quote within 24h.", ctaSubtitle: "Contact us with your product link, we'll send you a quote within 24h.",
ctaContact: "Contact us", ctaContact: "Contact us",
ctaMessenger: "Messenger" ctaMessenger: "Messenger"
},
confirmation: {
loadingTitle: "Confirming…",
loadingDesc: "We're finalising your registration and preparing your client reference number.",
loadingHint: "A few seconds…",
successTitle: "Registration confirmed!",
successDesc: "Welcome to MVA Global Fret. Your client reference number has been sent to you by email.",
successHint: "📧 Don't forget to check your spam folder.",
refLabel: "YOUR CLIENT REFERENCE NUMBER",
errorTitle: "Confirmation received",
errorDesc: "Your confirmation has been recorded with MVA Global Fret. If you have any question about your reference number, please contact us directly.",
backHome: "Back to home",
guide: "Shipping guide",
contactUs: "Contact us"
} }
}, },
@ -1007,6 +1037,21 @@ const translations = {
ctaSubtitle: "Mifandraisa aminay miaraka amin'ny rohin'ny vokatra, hanome tombam-bidy izahay ao anatin'ny 24 ora.", ctaSubtitle: "Mifandraisa aminay miaraka amin'ny rohin'ny vokatra, hanome tombam-bidy izahay ao anatin'ny 24 ora.",
ctaContact: "Mifandraisa aminay", ctaContact: "Mifandraisa aminay",
ctaMessenger: "Messenger" ctaMessenger: "Messenger"
},
confirmation: {
loadingTitle: "Eo am-panamafisana…",
loadingDesc: "Mamarana ny fisoratana anaranareo izahay ary manomana ny laharanao mpanjifa.",
loadingHint: "Segondra vitsivitsy…",
successTitle: "Voamarina ny fisoratana anarana!",
successDesc: "Tongasoa eto amin'ny MVA Global Fret. Nalefa tao amin'ny mailakao ny laharanao mpanjifa.",
successHint: "📧 Jereo koa ao amin'ny spam.",
refLabel: "NY LAHARANAO MPANJIFA",
errorTitle: "Voaray ny fanamafisana",
errorDesc: "Voarakitra tsara eto amin'ny MVA Global Fret ny fanamafisanareo. Raha manana fanontaniana mikasika ny laharana, mifandraisa aminay.",
backHome: "Hiverina any am-pandraisana",
guide: "Tari-dàlana fandefasana",
contactUs: "Mifandraisa aminay"
} }
} }
}; };