/* ============================================================================
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);
});
}
});
});
});
})();