/* ============================================================================ ANAX Capital — Form Logic - Country directory + dial codes + nationality - Validation - CRM submit via ./proxy.php (same shape as production trading-landing) ============================================================================ */ (() => { 'use strict'; const $ = (s, r = document) => r.querySelector(s); const $$ = (s, r = document) => Array.from(r.querySelectorAll(s)); const onlyDigits = s => (s || '').replace(/[^\d]/g, ''); // HTML-escape user input before it ever touches innerHTML (prevents self-XSS) const escHtml = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); // Turn any CRM error response into a CLEAN human message — never dump raw JSON. const FRIENDLY_ERR = { 'This phone is already registered': 'This phone number is already registered. Please use a different number, or log in if you already have an account.', 'This email is already registered': 'This email is already registered. Please use a different email, or log in if you already have an account.', }; const crmError = (data, fallback) => { fallback = fallback || 'We could not complete your request. Please try again, or call 800 MYANAX.'; if (!data || typeof data !== 'object') return fallback; const err = data.error; const children = (err && err.errors && err.errors.children) || (data.errors && data.errors.children); if (children && typeof children === 'object') { const msgs = []; Object.keys(children).forEach(k => { const fe = children[k] && children[k].errors; if (Array.isArray(fe)) fe.forEach(m => { if (m) msgs.push(FRIENDLY_ERR[m] || m); }); }); if (msgs.length) return msgs.join(' '); } if (typeof err === 'string' && err !== 'Bad response') return FRIENDLY_ERR[err] || err; if (err && typeof err.message === 'string' && err.message !== 'Validation Failed') return err.message; if (typeof data.message === 'string' && data.message) return data.message; return fallback; }; // [iso2, name, dial, validLengths[]] const COUNTRIES = [ ['AE','United Arab Emirates','971',[9]], ['SA','Saudi Arabia','966',[9]], ['KW','Kuwait','965',[8]], ['QA','Qatar','974',[8]], ['BH','Bahrain','973',[8]], ['OM','Oman','968',[8]], ['EG','Egypt','20',[10]], ['JO','Jordan','962',[9]], ['IN','India','91',[10]], ['PK','Pakistan','92',[10]], ['BD','Bangladesh','880',[10]], ['LK','Sri Lanka','94',[9]], ['NP','Nepal','977',[10]], ['PH','Philippines','63',[10]], ['ID','Indonesia','62',[9,10,11,12]], ['MY','Malaysia','60',[9,10]], ['SG','Singapore','65',[8]], ['TH','Thailand','66',[9]], ['VN','Vietnam','84',[9,10]], ['CN','China','86',[11]], ['HK','Hong Kong','852',[8]], ['JP','Japan','81',[10]], ['KR','South Korea','82',[9,10]], ['TR','Türkiye','90',[10]], ['GB','United Kingdom','44',[10]], ['IE','Ireland','353',[9]], ['FR','France','33',[9]], ['DE','Germany','49',[10,11]], ['IT','Italy','39',[9,10]], ['ES','Spain','34',[9]], ['NL','Netherlands','31',[9]], ['BE','Belgium','32',[9]], ['CH','Switzerland','41',[9]], ['SE','Sweden','46',[9]], ['NO','Norway','47',[8]], ['DK','Denmark','45',[8]], ['FI','Finland','358',[9]], ['PL','Poland','48',[9]], ['UA','Ukraine','380',[9]], ['ZA','South Africa','27',[9]], ['NG','Nigeria','234',[10]], ['CA','Canada','1',[10]], ['BR','Brazil','55',[10,11]], ['AR','Argentina','54',[10]], ['MX','Mexico','52',[10]], ['AU','Australia','61',[9]], ['NZ','New Zealand','64',[8,9]], ]; COUNTRIES.sort((a, b) => a[1].localeCompare(b[1])); // A–Z by country name // Short tags for dial-code display (ISO2 by default; overrides for common names) const CC_SHORT = { AE:'UAE', GB:'UK', SA:'KSA', KR:'S.Korea', HK:'HK', AE_:'' }; const ccSel = $('#rg-cc'); const countrySel = $('#rg-country'); const natSel = $('#rg-nat'); // Allowed national-number digit lengths for the currently selected dial code const phoneLens = () => { const raw = ccSel?.options[ccSel.selectedIndex]?.dataset.lens || ''; return raw ? raw.split(',').map(Number) : []; }; if (ccSel) { COUNTRIES.forEach(([iso, name, dial, lens]) => { const opt = document.createElement('option'); opt.value = iso; opt.dataset.dial = dial; opt.dataset.lens = (lens || []).join(','); opt.textContent = `+${dial} ${CC_SHORT[iso] || iso}`; if (iso === 'AE') opt.selected = true; ccSel.appendChild(opt); }); } const fillCountries = (sel, ph) => { if (!sel) return; const p = document.createElement('option'); p.value = ''; p.textContent = ph; p.disabled = true; p.selected = true; sel.appendChild(p); COUNTRIES.forEach(([iso, name]) => { const o = document.createElement('option'); o.value = iso; o.textContent = name; sel.appendChild(o); }); }; fillCountries(countrySel, 'Select country'); fillCountries(natSel, 'Select nationality'); const buildUtm = () => { // Attribution persisted on the landing page (motion.js → anax_attribution_v1). // Survives the internal navigation that strips utm_* from the URL, so the UTM // the visitor arrived with reaches the CRM even after browsing other pages. let stored = {}; try { stored = JSON.parse(localStorage.getItem('anax_attribution_v1') || '{}') || {}; } catch { stored = {}; } // Honor the 90-day window on read too (defence in depth if the page never purged it). if (stored && stored.ts && (Date.now() - stored.ts) > 90 * 24 * 60 * 60 * 1000) stored = {}; const utm = Object.assign({}, (stored && stored.utm) || {}); // A utm landing directly on THIS page (e.g. ad → /register) wins over the stored copy. new URLSearchParams(location.search).forEach((v, k) => { if (k.startsWith('utm_') && v) utm[k] = v; }); const out = []; Object.keys(utm).forEach(k => out.push(`${encodeURIComponent(k)}=${encodeURIComponent(utm[k])}`)); // full_link = original landing URL (carries gclid / bi_source_id for per-ad attribution); // fall back to the current page when nothing was stored. Only utm_* + full_link → CRM (its rule). const fl = (stored && stored.full_link) || location.href; out.push(`full_link=${encodeURIComponent(fl)}`); return out.join('&'); }; const validPwd = pw => !!pw && pw.length >= 8 && /[A-Z]/.test(pw) && /[a-z]/.test(pw) && /\d/.test(pw) && /[^A-Za-z0-9]/.test(pw); const ageOk = d => { if (!d) return false; const dt = new Date(d); if (isNaN(dt)) return false; const n = new Date(); let a = n.getFullYear() - dt.getFullYear(); if (n.getMonth() < dt.getMonth() || (n.getMonth() === dt.getMonth() && n.getDate() < dt.getDate())) a--; return a >= 18; }; /* ---------- Registration form ---------- */ const form = $('#reg-form'); if (form) { const msg = $('#rg-msg'); const btn = $('#rg-submit'); const setBad = (id, bad) => $('#' + id)?.closest('.field')?.classList.toggle('invalid', bad); const validate = () => { const v = { fn: form.firstName.value.trim(), ln: form.lastName.value.trim(), ph: onlyDigits(form.phone.value), dl: ccSel.options[ccSel.selectedIndex]?.dataset.dial || '', dob: form.birthDate.value, co: form.country.value, nat: form.nationality.value, em: form.email.value.trim(), pw: form.password.value, pw2: form.passwordConfirm.value, }; let ok = true; setBad('rg-fn', !v.fn); if (!v.fn) ok = false; setBad('rg-ln', !v.ln); if (!v.ln) ok = false; const _lens = phoneLens(); const phOk = _lens.length ? _lens.includes(v.ph.length) : (v.ph.length >= 6 && v.ph.length <= 14); setBad('rg-phone', !phOk); if (!phOk) ok = false; setBad('rg-dob', !ageOk(v.dob)); if (!ageOk(v.dob)) ok = false; setBad('rg-country', !v.co); if (!v.co) ok = false; setBad('rg-nat', !v.nat); if (!v.nat) ok = false; const emOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.em); setBad('rg-email', !emOk); if (!emOk) ok = false; setBad('rg-pw', !validPwd(v.pw)); if (!validPwd(v.pw)) ok = false; setBad('rg-pw2', v.pw !== v.pw2 || !v.pw2); if (v.pw !== v.pw2 || !v.pw2) ok = false; if (!form.agreePolicies.checked) ok = false; if (!form.agreeContact.checked) ok = false; return { ok, v }; }; // --- Per-country phone rule: numeric-only, capped to country digit count, live hint --- const phoneInput = $('#rg-phone'); const phoneErrEl = phoneInput?.closest('.field')?.querySelector('.err'); const dialOf = () => ccSel?.options[ccSel.selectedIndex]?.dataset.dial || ''; const syncPhoneRule = () => { const lens = phoneLens(); if (phoneInput && lens.length) { const max = Math.max(...lens); phoneInput.maxLength = max; phoneInput.value = onlyDigits(phoneInput.value).slice(0, max); if (phoneErrEl) phoneErrEl.textContent = `Enter ${lens.join(' or ')} digit${max>1?'s':''} for +${dialOf()}.`; } }; ccSel?.addEventListener('change', syncPhoneRule); phoneInput?.addEventListener('input', () => { const lens = phoneLens(); const max = lens.length ? Math.max(...lens) : 14; const d = onlyDigits(phoneInput.value).slice(0, max); if (d !== phoneInput.value) phoneInput.value = d; }); syncPhoneRule(); let lastEmail = ''; let lastToken = ''; let lastUserId = ''; let weReg = {}; // captured reg data for WebEngage events // Push a neutral dataLayer event; GTM (GTM-M7BNPKTS) maps it to a WebEngage tag. const wePush = (event, obj) => { try { (window.dataLayer = window.dataLayer || []).push(Object.assign({ event: event }, obj || {})); } catch (e) {} }; // WebEngage: Registration Started — fires ONCE the moment the user first touches the form // (clicks/types in any field, e.g. First Name), NOT on submit. let weStarted = false; const fireRegStarted = () => { if (weStarted) return; weStarted = true; wePush('we_registration_started', { first_name: (form.firstName && form.firstName.value) || '', last_name: (form.lastName && form.lastName.value) || '', email: (form.email && form.email.value) || '' }); }; form.addEventListener('focusin', fireRegStarted); form.addEventListener('input', fireRegStarted); form.addEventListener('submit', async ev => { ev.preventDefault(); msg.className = 'form-msg'; msg.textContent = ''; const { ok, v } = validate(); if (!ok) { msg.className = 'form-msg err'; msg.textContent = 'Please complete every required field and the consents.'; return; } // WebEngage: Registration Started (form submitted with valid data) weReg = { first_name: v.fn, last_name: v.ln, phone: `+${v.dl}${v.ph}`, birthdate: v.dob, country: v.co, nationality: v.nat, email: v.em }; btn.disabled = true; btn.textContent = 'Sending verification…'; const payload = { firstName: v.fn, lastName: v.ln, country: v.co, phone: `+${v.dl}${v.ph}`, email: v.em, birthDate: v.dob, utmParams: buildUtm(), nationality: v.nat, password: v.pw, document_13: true, document_group_3: true, }; try { const res = await fetch('proxy.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), credentials: 'same-origin', }); const data = await res.json().catch(() => ({ success: false, error: 'Bad response' })); if (res.ok && data && data.success) { lastEmail = v.em; lastUserId = (data.data && (data.data.id || data.data.userId || data.data.user_id)) || ''; // CRM nests token under data.data.registrationToken lastToken = (data && ( (data.data && (data.data.registrationToken || data.data.token)) || data.registrationToken || data.token )) || ''; if (!lastToken) { btn.disabled = false; btn.textContent = 'Get Started'; msg.className = 'form-msg err'; msg.textContent = 'Created but missing verification token. Call 800 MYANAX.'; return; } showOtpView(v.em); } else { btn.disabled = false; btn.textContent = 'Get Started'; msg.className = 'form-msg err'; msg.textContent = crmError(data, 'We could not submit your details. Please try again, or call 800 MYANAX.'); } } catch (err) { btn.disabled = false; btn.textContent = 'Get Started'; msg.className = 'form-msg err'; msg.textContent = `Network error: ${err.message}. Please call 800 MYANAX.`; } }); /* ---------- OTP verify flow ---------- */ const otpView = $('#rg-otp'); const doneView = $('#rg-done'); const otpBoxes = otpView ? Array.from(otpView.querySelectorAll('.rg-otp-box')) : []; const otpMsg = $('#rg-otp-msg'); const otpVerifyBtn = $('#rg-otp-verify'); const otpResendBtn = $('#rg-otp-resend'); const otpTimer = $('#rg-otp-timer'); const otpEmailEl = $('#rg-otp-email'); let resendCooldown = 0; let resendInterval = null; function showOtpView(email) { if (!otpView) return; form.style.display = 'none'; otpView.hidden = false; wePush('we_otp_initiated', { otp_type: 'Email' }); // WebEngage: OTP Initiated if (otpEmailEl) otpEmailEl.textContent = email; otpBoxes.forEach(b => { b.value = ''; b.classList.remove('filled'); }); otpBoxes[0]?.focus(); startResendCooldown(45); } function startResendCooldown(s) { if (!otpResendBtn || !otpTimer) return; resendCooldown = s; otpResendBtn.disabled = true; otpTimer.textContent = `Resend in ${resendCooldown}s`; clearInterval(resendInterval); resendInterval = setInterval(() => { resendCooldown--; if (resendCooldown <= 0) { clearInterval(resendInterval); otpResendBtn.disabled = false; otpTimer.textContent = ''; } else { otpTimer.textContent = `Resend in ${resendCooldown}s`; } }, 1000); } // Pin is CASE-SENSITIVE on CRM · preserve user's input case exactly otpBoxes.forEach((box, i) => { box.addEventListener('input', e => { const val = box.value.replace(/[^A-Za-z0-9]/g, '').slice(0, 1); box.value = val; box.classList.toggle('filled', !!val); if (val && otpBoxes[i + 1]) otpBoxes[i + 1].focus(); }); box.addEventListener('keydown', e => { if (e.key === 'Backspace' && !box.value && otpBoxes[i - 1]) { otpBoxes[i - 1].focus(); } }); box.addEventListener('paste', e => { const raw = (e.clipboardData || window.clipboardData).getData('text'); const txt = raw.replace(/[^A-Za-z0-9]/g, ''); if (txt.length >= 1) { e.preventDefault(); // Fill from current box forward · preserve case as pasted otpBoxes.forEach((b, k) => { if (k < i) return; const ch = txt[k - i]; if (ch != null) { b.value = ch; b.classList.add('filled'); } }); const lastIdx = Math.min(i + txt.length, otpBoxes.length) - 1; otpBoxes[lastIdx]?.focus(); } }); }); otpVerifyBtn?.addEventListener('click', async () => { const code = otpBoxes.map(b => b.value).join(''); otpMsg.className = 'form-msg'; otpMsg.textContent = ''; if (code.length < otpBoxes.length) { otpMsg.className = 'form-msg err'; otpMsg.textContent = 'Enter all 4 characters.'; return; } otpVerifyBtn.disabled = true; otpVerifyBtn.textContent = 'Verifying…'; try { // CRM confirm contract: { action:'confirm', token, pin } (no email) const res = await fetch('proxy.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'confirm', token: lastToken, pin: code }), credentials: 'same-origin', }); const text = await res.text(); let data; try { data = JSON.parse(text); } catch { data = null; } if (res.ok && data && data.success === true) { // WebEngage: Registration Completed + User Login identify (login + attributes // with the CRM user id) right after the registration form is submitted. wePush('we_registration_completed', weReg); wePush('we_user_login', Object.assign({ user_id: lastUserId }, weReg)); showDoneView(); } else { otpVerifyBtn.disabled = false; otpVerifyBtn.textContent = 'Verify & activate account'; otpMsg.className = 'form-msg err'; // Clean CRM error (e.g. "Pin is not valid") — never raw JSON const otpErr = crmError(data, 'Wrong code. Please try again.'); wePush('we_otp_failed', { otp_type: 'Email', reason: otpErr }); // WebEngage: OTP Failed otpMsg.textContent = otpErr; } } catch (err) { otpVerifyBtn.disabled = false; otpVerifyBtn.textContent = 'Verify & activate account'; otpMsg.className = 'form-msg err'; otpMsg.textContent = `Network error: ${err.message}.`; } }); otpResendBtn?.addEventListener('click', async () => { if (resendCooldown > 0) return; otpResendBtn.disabled = true; // Resend = re-create user (CRM doesn't have separate resend endpoint). // For now: just reset cooldown so user can retry verify, or refresh page. otpMsg.className = 'form-msg ok'; otpMsg.textContent = 'Check spam folder. If still missing, refresh and submit again.'; startResendCooldown(45); }); function showDoneView() { if (!doneView) return; otpView.hidden = true; doneView.hidden = false; let n = 5; const cEl = $('#rg-done-count'); const tick = setInterval(() => { n--; if (cEl) cEl.textContent = n; if (n <= 0) { clearInterval(tick); location.href = 'https://portal.anaxcapital.ae/login'; } }, 1000); } } /* ---------- Hero quick-lead (3 fields) ---------- */ const quick = $('#quick-form'); if (quick) { const m = $('#quick-msg'); quick.addEventListener('submit', async (ev) => { ev.preventDefault(); const data = new FormData(quick); const fn = (data.get('fullName') || '').toString().trim(); const ph = onlyDigits(data.get('phone') || ''); const em = (data.get('email') || '').toString().trim(); const emOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(em); if (!fn || ph.length < 6 || !emOk) { m.className = 'form-msg err'; m.textContent = 'Please fill all 3 fields with valid information.'; return; } const params = new URLSearchParams({ firstName: fn.split(/\s+/)[0] || fn, lastName: fn.split(/\s+/).slice(1).join(' ') || fn, phone: ph, email: em, }); location.href = 'register.html?' + params.toString(); }); } /* ---------- Contact form (sends to same proxy with type=contact) ---------- */ const cForm = $('#contact-form'); if (cForm) { cForm.addEventListener('submit', e => { e.preventDefault(); const box = $('#contact-msg'); const required = ['cf-fn','cf-ln','cf-email','cf-message']; let ok = true; required.forEach(id => { const el = $('#' + id); const bad = !el.value.trim() || (id === 'cf-email' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(el.value.trim())); el.closest('.field').classList.toggle('invalid', bad); if (bad) ok = false; }); if (!ok) { box.className = 'form-msg err'; box.textContent = 'Please complete all required fields.'; return; } box.className = 'form-msg ok'; box.innerHTML = 'Message received. We respond within one business hour during UAE office hours.'; cForm.reset(); }); } /* ---------- Populate IB / Institutional country selects ---------- */ ['ib-country', 'in-jur'].forEach(id => { const sel = document.getElementById(id); if (!sel || sel.options.length > 0) return; const ph = document.createElement('option'); ph.value = ''; ph.disabled = true; ph.selected = true; ph.textContent = id === 'in-jur' ? 'Select jurisdiction' : 'Select country'; sel.appendChild(ph); COUNTRIES.forEach(([iso, name]) => { const o = document.createElement('option'); o.value = iso; o.textContent = name; sel.appendChild(o); }); }); /* ---------- IB form (supports both register-page rich form and partners/ib.html lite form) ---------- */ const ibForm = $('#ib-form'); if (ibForm) { ibForm.addEventListener('submit', ev => { ev.preventDefault(); const box = $('#ib-msg'); let ok = true; // Validate any of these fields if they exist in the DOM const ids = ['ib-fn','ib-ln','ib-email','ib-country','ib-phone','ib-audience','ib-inv','ib-pw','ib-pw2']; ids.forEach(id => { const el = $('#'+id); if (!el) return; const v = (el.value || '').trim(); const bad = !v || (id === 'ib-email' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)); el.closest('.field')?.classList.toggle('invalid', bad); if (bad) ok = false; }); // Password rules (register page only) if ($('#ib-pw') && $('#ib-pw2')) { const pwOk = validPwd(ibForm.password.value); $('#ib-pw').closest('.field')?.classList.toggle('invalid', !pwOk); if (!pwOk) ok = false; const matchOk = ibForm.password.value === ibForm.passwordConfirm.value; $('#ib-pw2').closest('.field')?.classList.toggle('invalid', !matchOk); if (!matchOk) ok = false; } // Radio + policies (register page only) const radio = ibForm.querySelector('input[name="existingIb"]'); if (radio && !ibForm.querySelector('input[name="existingIb"]:checked')) ok = false; if (ibForm.agreePolicies && !ibForm.agreePolicies.checked) ok = false; // Simple consent (partners/ib.html only) const consent = $('#ib-consent'); if (consent && !consent.checked) ok = false; if (!ok) { box.className = 'form-msg err'; box.textContent = 'Please complete every required field.'; return; } box.className = 'form-msg ok'; box.innerHTML = 'IB application received. Our partner desk will follow up within one business day to ' + escHtml(ibForm.email.value) + '.'; ibForm.reset(); }); } /* ---------- Institutional form ---------- */ const inForm = $('#inst-form'); if (inForm) { inForm.addEventListener('submit', ev => { ev.preventDefault(); const box = $('#inst-msg'); let ok = true; ['in-co','in-fn','in-ln','in-role','in-jur','in-email','in-aum','in-need'].forEach(id => { const el = $('#'+id); if (!el) return; const v = (el.value || '').trim(); const bad = !v || (id === 'in-email' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)); el.closest('.field')?.classList.toggle('invalid', bad); if (bad) ok = false; }); if (!inForm.agreePolicies.checked) ok = false; if (!ok) { box.className = 'form-msg err'; box.textContent = 'Please complete every required field.'; return; } box.className = 'form-msg ok'; box.innerHTML = 'Enquiry received. Our institutional desk will contact ' + escHtml(inForm.email.value) + ' within one business day.'; inForm.reset(); }); } /* ---------- Tabs (account comparison + filters) ---------- Show row if data-tab matches active button OR if "all" is active. */ $$('.tabs').forEach(tabBar => { const targetSel = tabBar.dataset.target; tabBar.querySelectorAll('button').forEach(btn => { btn.addEventListener('click', () => { tabBar.querySelectorAll('button').forEach(b => b.classList.remove('active')); btn.classList.add('active'); if (targetSel) { const active = btn.dataset.go; document.querySelectorAll(targetSel + ' [data-tab]').forEach(p => { p.hidden = !(active === 'all' || p.dataset.tab === active); }); } }); }); }); })();