const CC_FORM_CONTAINER_ID = 'captain_compliance_form_container'; const CC_FORM_ID = 'captain_compliance_form'; const CC_SERVER_URL = 'https://cc-platform-api-prod.fly.dev'; const CC_ACCESS_TOKEN = document.currentScript.getAttribute('access-token'); const MIN_SUBMIT_MS = 2500; // minimum time window to accept a submission (anti-bot) let ccFormInitialized = false; const CC_ELEMENT_TYPES = { TEXT: { element: ({ code }) => ` `, }, EMAIL: { element: ({ code }) => ` `, }, PHONE: { element: ({ code }) => ` `, }, NUMBER: { element: ({ code }) => ` `, }, TEXTAREA: { element: ({ code }) => ` `, }, FILE: { element: ({ code }) => ` `, }, DROPDOWN: { element: ({ code, metadata }) => ` `, }, SELECT: { element: ({ code, metadata }) => ` `, }, }; async function loadCountries() { const response = await fetch(`${CC_SERVER_URL}/country`, { method: 'GET', headers: { Accept: 'application/json' }, }); if (!response.ok) { throw new Error(`Failed to fetch countries: ${response.status}`); } return response.json(); } async function loadStates(idCountry) { const response = await fetch( `${CC_SERVER_URL}/state/${encodeURIComponent(idCountry)}`, { method: 'GET', headers: { Accept: 'application/json' }, }, ); if (!response.ok) { throw new Error(`Failed to fetch states: ${response.status}`); } return response.json(); } function pickTextColor(bgHex) { // bgHex: e.g. "#1a73e8" const hex = bgHex.replace('#', ''); const r = parseInt(hex.slice(0, 2), 16); const g = parseInt(hex.slice(2, 4), 16); const b = parseInt(hex.slice(4, 6), 16); // YIQ: > 128 ⇒ usar negro, si no blanco const yiq = (r * 299 + g * 587 + b * 114) / 1000; return yiq >= 128 ? '#000' : '#FFF'; } function addStyles(styles) { const themeColor = styles.theme; const fontColor = styles.fontColor; const borderRadius = styles.borderRadius; const borderWidth = styles.borderWidth; const textColor = pickTextColor(themeColor); const someStyle = ` `; document.head.insertAdjacentHTML('beforeend', someStyle); } async function loadFormData(accessToken) { const response = await fetch( `${CC_SERVER_URL}/requestForm/form-token?access-token=${accessToken}`, ); return await response.json(); } function handleSubmitButton() { const submitButton = document.getElementById('cc-form-submit-button'); submitButton.disabled = true; submitButton.value = 'Submitting...'; submitButton.classList.add('cc-default-btn-disabled'); return () => { submitButton.disabled = false; submitButton.value = 'Submit'; submitButton.classList.remove('cc-default-btn-disabled'); }; } function setFieldError(input, errorEl, message) { if (!input || !errorEl) return; if (message) { errorEl.textContent = message; errorEl.style.display = 'block'; input.classList.add('cc-form-input-error'); input.setAttribute('aria-invalid', 'true'); input.setAttribute('aria-describedby', errorEl.id); } else { errorEl.textContent = ''; errorEl.style.display = 'none'; input.classList.remove('cc-form-input-error'); input.removeAttribute('aria-invalid'); input.removeAttribute('aria-describedby'); } } function sendFormRequest(requestFormId, data, cb) { const formData = new FormData(); formData.append('data', JSON.stringify(data)); formData.append('requestFormId', requestFormId); fetch(`${CC_SERVER_URL}/requestFormSubmit`, { method: 'POST', body: formData, }) .then((response) => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(() => { cb(); }) .catch((error) => { console.error('There was a problem with the API request:', error.message); cb(); }); } // Creates an off-screen text input honeypot to trap bots; kept visible in DOM (not hidden) // so automated fillers are more likely to write into it. Silent failure prevents signal to bots. function createHoneypotField(form) { const name = `company_${Math.random().toString(36).slice(2, 8)}`; const input = document.createElement('input'); input.type = 'text'; input.name = name; input.id = name; input.autocomplete = 'off'; input.tabIndex = -1; input.style.position = 'absolute'; input.style.left = '-9999px'; input.style.top = 'auto'; input.style.width = '1px'; input.style.height = '1px'; input.style.overflow = 'hidden'; form.dataset.ccHoneypotName = name; form.dataset.ccInitTs = String(Date.now()); form.appendChild(input); return input; } function isTooFast(form) { const started = Number(form.dataset.ccInitTs || Date.now()); return Date.now() - started < MIN_SUBMIT_MS; } function normalizeFields(fields) { return (fields || []) .filter(({ disabled }) => !disabled) .sort((a, b) => a?.order - b?.order); } function attachFormBehavior({ form, fields, id }) { if (!form || !fields || form.dataset.ccSubmitAttached === 'true') return; const fieldsToRender = normalizeFields(fields); form.dataset.ccSubmitAttached = 'true'; form.dataset.ccFields = JSON.stringify(fieldsToRender); const stateCache = new Map(); const existingHoneypot = form.querySelector(`input[name="${form.dataset.ccHoneypotName || ''}"]`) || null; const honeypotInput = existingHoneypot || createHoneypotField(form); // Attach dependent select logic: country -> state const countrySelect = form.querySelector('#country'); const stateSelect = form.querySelector('#state'); const renderStates = (states) => { if (!stateSelect) return; const options = `` + states .map(({ value, label }) => ``) .join(''); stateSelect.innerHTML = options; stateSelect.disabled = false; }; if (stateSelect) { stateSelect.disabled = true; stateSelect.innerHTML = ''; } if (countrySelect && stateSelect) { countrySelect.addEventListener('change', async (event) => { const countryId = event.target.value; if (!countryId) { stateSelect.disabled = true; stateSelect.innerHTML = ''; return; } stateSelect.disabled = true; stateSelect.innerHTML = ''; try { const states = stateCache.get(countryId) || (await loadStates(countryId)).map(({ id, name }) => ({ value: id, label: name, })); stateCache.set(countryId, states); renderStates(states); } catch (error) { console.error('[CC FORM] Failed to load states', error); stateSelect.innerHTML = ''; stateSelect.disabled = true; } }); } form.addEventListener('submit', (event) => { event.preventDefault(); let done = () => { console.log('done'); }; try { if (honeypotInput.value.trim() !== '' || isTooFast(form)) { console.warn('[CC FORM] Submit blocked (honeypot/too fast)', { honeypotFilled: honeypotInput.value.trim() !== '', elapsedMs: Date.now() - Number(form.dataset.ccInitTs || Date.now()), }); return; } let isValid = true; const formData = {}; for (const { code, label, type } of fieldsToRender) { const input = document.getElementById(code); const error = document.getElementById(`${code}Error`); if (!input || !error) { console.error('[CC FORM] Missing input or error node', { code, type, }); isValid = false; continue; } if (type === 'EMAIL') { const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailPattern.test(input.value)) { setFieldError(input, error, 'Please enter a valid email address'); isValid = false; } else { formData[code] = { type, code, label, value: input.value }; setFieldError(input, error, ''); } } else if (type === 'PHONE') { const normalized = (input.value || '').replace(/[^\d]/g, ''); const phonePattern = /^\+?[0-9\s().-]{7,20}$/; const hasValidFormat = phonePattern.test(input.value); const hasValidLength = normalized.length >= 7 && normalized.length <= 20; if (!hasValidFormat || !hasValidLength) { setFieldError(input, error, 'Please enter a valid phone number'); isValid = false; } else { formData[code] = { type, code, label, value: input.value }; setFieldError(input, error, ''); } } else if (type === 'NUMBER') { const age = parseInt(input.value); if (isNaN(age) || age <= 0) { setFieldError( input, error, `Please enter a valid ${label.toLowerCase()}`, ); isValid = false; } else { formData[code] = { type, code, label, value: input.value }; setFieldError(input, error, ''); } } else { if (input.value.trim() === '') { setFieldError( input, error, `${label.charAt(0).toUpperCase() + label.slice(1)} is required`, ); isValid = false; } else { formData[code] = { type, code, label, value: input.value }; setFieldError(input, error, ''); } } } if (!isValid) return; done = handleSubmitButton(); // Include honeypot value so backend can also ignore spam server-side formData[honeypotInput.name] = { type: 'HONEYPOT', code: honeypotInput.name, label: honeypotInput.name, value: honeypotInput.value || '', }; sendFormRequest(id, formData, () => { form.reset(); done(); }); } catch (error) { console.error('[CC FORM] Submit handler failed', error); done(); } }); } function createForm({ fields, description, id }) { const formContainer = document.getElementById(CC_FORM_CONTAINER_ID); if (!formContainer) return; if (document.getElementById(CC_FORM_ID)) { return; } const fieldsToRender = normalizeFields(fields); const inputs = fieldsToRender .map(({ label, code, type, metadata }) => { const component = CC_ELEMENT_TYPES[type]; if (!component) { console.error('[CC FORM] Unknown field type; skipping', { code, type, }); return ''; } return `
${description}
` : ''}