1// const GAS_URL = 'https://script.google.com/macros/s/AKfycbzigXMD-m8aNyy4juOjjG2SM7YTwRweiCV3hgALEMwyVCoM19O6RG_e9vTnedQaeJFA/exec'; 2const GAS_URL = 'https://script.google.com/macros/s/AKfycbwIhT1FIjWDpyqBX_FBIMyfFSLE8Gv57wSInCikMktlTF0cSVrDj8Eo7L38FEWSH12G/exec'; 3function postToSheetSimple({ amount, currency, recurrence } = {}) { 4 const countrySel = document.getElementById('country'); 5 6 // Si no recibimos amount/currency, tomarlos de la selección actual 7 if (amount == null || !currency) { 8 const sel = typeof currentSelection === 'function' ? currentSelection() : null; 9 if (!sel) return; 10 currency = currency || (sel.currency || 'USD'); 11 amount = (currency === 'USD') ? (+sel.amountUSD || 0) : (Math.round(sel.amountCRC) || 0); 12 recurrence = recurrence || sel.recurrence || 'unica'; // mensual | anual | unica 13 } 14 15 // Formato con sÃmbolo según moneda 16 const amountFormatted = (currency === 'USD') 17 ? `$${(+amount).toFixed(2)}` 18 : `â¡${Math.round(amount)}`; 19 20 const payload = { 21 firstName: firstName?.value?.trim() || '', 22 lastName: lastName?.value?.trim() || '', 23 email: email?.value?.trim() || '', 24 countryCode: countrySel?.value || '', 25 countryName: countrySel?.selectedOptions?.[0]?.text || '', 26 region: region?.value?.trim() || '', 27 amount: amountFormatted, 28 type: recurrence, 29 phone: phone?.value?.trim() || '', 30 }; 31 32 // fire-and-forget para evitar CORS 33 fetch(GAS_URL, { 34 method: 'POST', 35 mode: 'no-cors', 36 // con no-cors solo se permiten "simple headers": 37 headers: { 'Content-Type': 'text/plain;charset=utf-8' }, 38 body: JSON.stringify(payload) 39 }); 40} 41 42// Helper translate 43const t = (en, es) => (/^es/i.test(document.documentElement.lang || '')) ? es : en; 44 45function resetDonationForm() { 46 try { form && form.reset(); } catch(e) {} 47 48 if (cardName) cardName.value = ''; 49 if (cardNumber) cardNumber.value = ''; 50 if (cardExpiry) cardExpiry.value = ''; 51 if (cardCvc) cardCvc.value = ''; 52 if (payErrors) payErrors.textContent = ''; 53 //validatePayment(); 54} 55 56 57/* Donations form logic â works with the shortcode HTML/CSS (no markup changes) */ 58(function () { 59 // ---------- helpers ---------- 60 const $ = (s, r=document) => r.querySelector(s); 61 const $$ = (s, r=document) => Array.from(r.querySelectorAll(s)); 62 63 // wrapper with data-* 64 const shell = document.querySelector('.shell[data-monthly-crc]') || document.getElementById('vg-donations') || document.getElementById('shell') || document.body; 65 66 // config from shortcode (fallbacks just in case) 67 const fxRate = parseFloat(shell?.dataset?.fx || '500'); 68 const PRESETS = { 69 mensual: { 70 crc: parseInt(shell?.dataset?.monthlyCrc || shell?.dataset?.monthly_crc || '3500', 10), 71 usd: parseFloat(shell?.dataset?.monthlyUsd || shell?.dataset?.monthly_usd || '7') 72 }, 73 anual: { 74 crc: parseInt(shell?.dataset?.yearlyCrc || shell?.dataset?.yearly_crc || '35700', 10), 75 usd: parseFloat(shell?.dataset?.yearlyUsd || shell?.dataset?.yearly_usd || '71') 76 } 77 }; 78 79 // elements (step 1) 80 const presetRadios = $$('input[name="preset"]'); // mensual | anual 81 const curCRC = $('#curCRC'); 82 const curUSD = $('#curUSD'); 83 const currencyPrefix = $('#currencyPrefix'); 84 const amountInput = $('#amountCustom'); 85 const convertedLabel = $('#convertedAmount'); 86 const customBox = amountInput?.closest('.custom-inner') || amountInput?.closest('.custom'); 87 88 const freqRadios = $$('input[name="customFreq"]'); // mensual | anual | unica (optional) 89 90 const btnToPersonal = $('#btnToPersonal'); 91 92 // elements (step 2) 93 const firstName = $('#firstName'); 94 const lastName = $('#lastName'); 95 const email = $('#email'); 96 const country = $('#country'); 97 const phone = $('#phone'); 98 const region = $('#region'); 99 const btnToPayment = $('#btnToPayment'); 100 101 // elements (step 3) 102 const cardName = $('#cardName'); 103 const cardNumber = $('#cardNumber'); 104 const cardExpiry = $('#cardExpiry'); 105 const cardCvc = $('#cardCvc'); 106 const payErrors = $('#payErrors'); 107 const btnSubmit = $('#btnSubmit'); 108 const form = $('#subscribeForm'); 109 const resultBox = $('#result'); 110 111 // stepper + panels 112 const panels = [$('#panel-plan'), $('#panel-personal'), $('#panel-payment')]; 113 const segs = $$('.seg'); 114 115 // ---------- state ---------- 116 const state = { 117 step: 0, 118 preset: 'mensual', // 'mensual' | 'anual' | null 119 customActive: false, // true when âOtro montoâ is the selected plan 120 customFreq: null, // 'mensual' | 'anual' | 'unica' | null (optional) 121 currency: 'CRC', // input currency for custom 122 amount: 0, // numeric amount in the current input currency 123 fxRate 124 }; 125 126 // ---------- formatting & conversions ---------- 127 const fmtUSD = (n) => { 128 const x = (typeof n === 'number' ? n : parseFloat(n) || 0);
129 return `$${x.toLocaleString('es-CR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; 130 }; 131 const fmtCRC = (n) => { 132 const x = Math.round(typeof n === 'number' ? n : parseFloat(n) || 0); 133 return `â¡${x.toLocaleString('de-DE')}`; 134 }; 135 const toUSD = (val, cur) => cur === 'USD' ? val : (val / state.fxRate); 136 const toCRC = (val, cur) => cur === 'CRC' ? val : Math.round(val * state.fxRate); 137 138 // ---------- stepper ---------- 139 function setStep(n) { 140 state.step = Math.max(0, Math.min(2, n)); 141 panels.forEach((p, i) => p && p.classList.toggle('active', i === state.step)); 142 segs.forEach((s, i) => s && s.classList.toggle('active', i === state.step)); 143 const first = panels[state.step]?.querySelector('input,select,button'); 144 if (first) first.focus({ preventScroll: true }); 145 } 146 147 // ---------- PLAN SELECTION ---------- 148 // default: mensual preset active 149 const mensualRadio = $('input[name="preset"][value="mensual"]'); 150 if (mensualRadio) { 151 mensualRadio.checked = true; 152 state.preset = 'mensual'; 153 state.customActive = false; 154 } 155 156 function activatePreset(value) { 157 state.preset = value; // 'mensual' | 'anual' 158 state.customActive = false; 159 // clear custom 160 state.amount = 0; 161 if (amountInput) amountInput.value = ''; 162 convertedLabel && (convertedLabel.textContent = state.currency === 'CRC' ? '$0.00' : 'â¡0'); 163 freqRadios.forEach(r => (r.checked = false)); 164 updatePlanCTA(); 165 } 166 167 function activateCustom() { 168 // Custom is a plan: clear preset selection 169 presetRadios.forEach(p => (p.checked = false)); 170 state.preset = null; 171 state.customActive = true; 172 updatePlanCTA(); 173 } 174 175 // when choosing preset tiles 176 presetRadios.forEach(r => { 177 r.addEventListener('change', () => { 178 if (r.checked) activatePreset(r.value); 179 }); 180 }); 181 182 // any interaction in custom area selects custom plan 183 if (customBox) { 184 customBox.addEventListener('click', (e) => { 185 // avoid fighting with input events (we just ensure it's active) 186 if (!state.customActive) activateCustom(); 187 }); 188 } 189 190 // custom currency toggle (radios) 191 function onCurrencyChange(newCur) { 192 if (state.currency === newCur) return; 193 // Convert the number already typed to the new currency so the value remains equivalent 194 if (state.amount > 0) { 195 const converted = newCur === 'USD' 196 ? +(toUSD(state.amount, 'CRC')).toFixed(2) // CRC -> USD 197 : Math.round(toCRC(state.amount, 'USD')); // USD -> CRC (round colones) 198 state.amount = converted; 199 if (amountInput) { 200 amountInput.value = newCur === 'USD' 201 ? String(converted.toFixed(2)) 202 : String(converted); // integer for CRC 203 } 204 } 205 state.currency = newCur; 206 currencyPrefix && (currencyPrefix.textContent = newCur === 'USD' ? '$' : 'â¡'); 207 updateConvertedDisplay(); 208 updatePlanCTA(); 209 } 210 211 curCRC && curCRC.addEventListener('change', () => { if (curCRC.checked) { activateCustom(); onCurrencyChange('CRC'); } }); 212 curUSD && curUSD.addEventListener('change', () => { if (curUSD.checked) { activateCustom(); onCurrencyChange('USD'); } }); 213 214 // custom amount typing 215 if (amountInput) { 216 amountInput.addEventListener('focus', activateCustom); 217 amountInput.addEventListener('input', (e) => { 218 activateCustom(); 219 const raw = (e.target.value || '').replace(/[^\d.,]/g, '').replace(',', '.'); 220 state.amount = parseFloat(raw) || 0; 221 updateConvertedDisplay(); 222 updatePlanCTA(); 223 }); 224 } 225 226 // optional custom frequency 227 freqRadios.forEach(fr => { 228 fr.addEventListener('change', () => { 229 activateCustom(); 230 state.customFreq = fr.value; // mensual | anual | unica 231 updatePlanCTA(); 232 }); 233 }); 234 235 // reflect opposite currency below the input 236 function updateConvertedDisplay() { 237 if (!convertedLabel) return; 238 if (!state.amount || state.amount <= 0) { 239 convertedLabel.textContent = state.currency === 'CRC' ? '$0.00' : 'â¡0'; 240 return; 241 } 242 convertedLabel.textContent = state.currency === 'CRC' 243 ? fmtUSD(toUSD(state.amount, 'CRC')) 244 : fmtCRC(toCRC(state.amount, 'USD')); 245 } 246 247 // enable âContinuarâ rules: 248 // - preset selected -> enabled 249 // - custom selected -> enabled only if amount > 0 250 function selectionValid() { 251 if (state.customActive) return state.amount > 0; 252 return !!state.preset; 253 } 254 function updatePlanCTA() { 255 if (btnToPersonal) btnToPersonal.disabled = !selectionValid(); 256 } 257 258 // ---------- STEP NAV ---------- 259 $('#btnToPersonal')?.addEventListener('click', () => setStep(1)); 260 $('#backToPlan')?.addEventListener('click', () => setStep(0)); 261 $('#btnToPayment')?.addEventListener('click', () => setStep(2)); 262 $('#backToPersonal')?.addEventListener('click', () => setStep(1)); 263 264 // ---------- STEP 2 validation ---------- 265 function validatePersonal() { 266 const ok = (firstName?.value?.trim() || '') && 267 (lastName?.value?.trim() || '') && 268 (email && /.+@.+\..+/.test(email.value)) && 269 (country && country.value) && 270 (phone?.value?.trim() || ''); 271 if (btnToPayment) btnToPayment.disabled = !ok; 272 } 273 [firstName, lastName, email, country, phone].forEach(el => el && el.addEventListener('input', validatePersonal)); 274
275 // ---------- STEP 3 masks & validation ---------- 276 function luhn(num) { 277 let s=0,d=false; 278 for (let i=num.length-1;i>=0;i--) { 279 let n=parseInt(num[i],10); 280 if (d){ n*=2; if (n>9) n-=9; } 281 s+=n; d=!d; 282 } 283 return s%10===0; 284 } 285 function maskCardNumber() { 286 if (!cardNumber) return; 287 const d = cardNumber.value.replace(/\D+/g,'').slice(0,19); 288 cardNumber.value = (d.match(/.{1,4}/g)||[]).join(' '); 289 validatePayment(); 290 } 291 function maskExpiry() { 292 if (!cardExpiry) return; 293 let d = cardExpiry.value.replace(/\D+/g,'').slice(0,4); 294 if (d.length>=3) d = `${d.slice(0,2)}/${d.slice(2)}`; 295 cardExpiry.value = d; 296 validatePayment(); 297 } 298 function maskCvc() { 299 if (!cardCvc) return; 300 cardCvc.value = cardCvc.value.replace(/\D+/g,'').slice(0,4); 301 validatePayment(); 302 } 303 function validatePayment() { 304 if (!btnSubmit) return; 305 payErrors && (payErrors.textContent = ''); 306 const nameOk = cardName?.value?.trim().length > 2; 307 const digits = (cardNumber?.value || '').replace(/\D+/g,''); 308 const cardOk = digits.length>=13 && digits.length<=19 && luhn(digits); 309 const exp = cardExpiry?.value || ''; 310 const expOk = /^\d{2}\/\d{2}$/.test(exp) && (function(){ 311 const [mm,yy]=exp.split('/').map(n=>parseInt(n,10)); 312 if (mm<1 || mm>12) return false; 313 const now=new Date(); const y=2000+yy; 314 const last=new Date(y,mm,0,23,59,59); 315 return last>=now; 316 })(); 317 const cvcOk = (cardCvc?.value || '').replace(/\D+/g,'').length>=3; 318 const ok = nameOk && cardOk && expOk && cvcOk; 319 btnSubmit.disabled = !ok; 320 if (!ok && cardNumber?.value && !cardOk && payErrors) payErrors.textContent = t('Invalid card number.', 'Número de tarjeta inválido.'); 321 } 322 cardNumber && cardNumber.addEventListener('input', maskCardNumber); 323 cardExpiry && cardExpiry.addEventListener('input', maskExpiry); 324 cardCvc && cardCvc.addEventListener('input', maskCvc); 325 cardName && cardName.addEventListener('input', validatePayment); 326 327 // ---------- selection snapshot ---------- 328 function currentSelection() { 329 // returns { type:'preset'|'custom', recurrence:'mensual'|'anual'|'unica', amountUSD, amountCRC, currency } 330 if (!state.customActive && state.preset) { 331 const p = PRESETS[state.preset]; 332 return { 333 type: 'preset', 334 recurrence: state.preset, // mensual | anual 335 amountUSD: p.usd, 336 amountCRC: p.crc, 337 currency: 'USD' // backend clarity for recurring 338 }; 339 } 340 if (state.customActive) { 341 if (!(state.amount > 0)) return null; 342 const amountUSD = toUSD(state.amount, state.currency); 343 const amountCRC = toCRC(state.amount, state.currency); 344 // if user didn't pick a frequency, default to single 345 const rec = state.customFreq || 'unica'; 346 return { 347 type: 'custom', 348 recurrence: rec, // mensual | anual | unica 349 amountUSD: +amountUSD.toFixed(2), 350 amountCRC: Math.round(amountCRC), 351 currency: state.currency 352 }; 353 } 354 return null; 355 } 356 357 function expParts() { 358 const [mm, aa] = (cardExpiry?.value || '00/00').split('/'); 359 return { month: parseInt(mm||'0',10), year: parseInt(aa||'0',10) }; 360 } 361 362 // ---------- Ajax & crypto ---------- 363 async function wpAjax(action, data = {}) { 364 const body = new URLSearchParams({ action, ...data }); 365 const res = await fetch('/wp-admin/admin-ajax.php', { 366 method: 'POST', 367 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 368 body 369 }); 370 const txt = await res.text(); 371 try { return JSON.parse(txt); } catch { return { raw: txt }; } 372 } 373 374 function rsaPublicKey() { 375 return "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCX8W5M3VE68PQrDir8nmkUSbMa qREAoJ5H/Nw/hSEtW/oBVRgihHfZY2Rd3p67k2pvKNRCYOlfmR/pshO6iJg7OZyT lXfT1EZDA3I+yim2a1TTi9Kb0RC/3MPgXzIO/hHi2yoEwLihoP1shpLWeCSkVbfD AulwpcdmHOO5kExj7QIDAQAB"; 376 } 377 function ensureCrypto() { 378 if (typeof window.JSEncrypt === 'undefined' || typeof window.aesjs === 'undefined') { 379 throw new Error('Faltan librerÃas de cifrado (JSEncrypt y aes-js). Cárgalas antes de procesar pagos.'); 380 } 381 } 382 function pack(obj, pair_) { 383 const aesjs = window.aesjs; 384 const JSEncrypt = window.JSEncrypt; 385 const pair = (pair_ !== undefined) ? pair_ : generateAESPairs(); 386 const textBytes = aesjs.utils.utf8.toBytes(JSON.stringify(obj)); 387 const aesCtr = new aesjs.ModeOfOperation.ctr(pair.k, new aesjs.Counter(pair.s)); 388 const encryptedBytes = aesCtr.encrypt(textBytes);
389 const encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes); 390 const encrypt = new JSEncrypt(); 391 encrypt.setPublicKey(rsaPublicKey()); 392 return { ld: encryptedHex, lk: encrypt.encrypt(JSON.stringify(pair)) }; 393 } 394 function generateAESPairs() { 395 const key = []; let iv = 0; 396 for (let k=0;k<16;k++) key.push(Math.floor(Math.random()*255)); 397 for (let k=0;k<16;k++) iv = Math.floor(Math.random()*255); 398 return { k: key, s: iv }; 399 } 400 401 // ---------- payment flows ---------- 402 async function startRecurring(sel) { 403 const every = sel.recurrence === 'mensual' ? 1 : 12; 404 const { month, year } = expParts(); 405 const card = (cardNumber?.value || '').replace(/\s+/g,''); 406 try { 407 ensureCrypto(); 408 const enc = pack({ 409 card: { 410 cardHolder: cardName.value.trim(), 411 expirationDate: { month, year }, 412 cardNumber: card, 413 cvc: cardCvc.value.trim(), 414 nickname: cardName.value.trim() 415 } 416 }); 417 418 // 1) session_payment 419 const sess = await wpAjax('session_payment'); 420 if (!sess?.session || !sess?.token) throw new Error('No se pudo iniciar la sesión de pago'); 421 422 // 2) get_token 423 const tok = await wpAjax('get_token', { session_code: sess.session, token: sess.token, ld: enc.ld, lk: enc.lk }); 424 const encrypted_card = tok?.result?.token; 425 if (!encrypted_card) throw new Error('No se pudo tokenizar la tarjeta'); 426 427 const countrySel = document.getElementById('country'); 428 const countryCode = countrySel?.value || 'CR'; // e.g. "CR" 429 const countryName = countrySel?.selectedOptions?.[0]?.text || 'Costa Rica';// e.g. "Costa Rica" 430 431 432 // 3) membership_payment (amount in USD) 433 const mp = await wpAjax('membership_payment', { 434 encrypted_card, 435 amountSelected: sel.amountUSD,
436 title: `Suscripción ${sel.recurrence}`, 437 every, 438 formName: `${firstName?.value?.trim() || ''} ${lastName?.value?.trim() || ''}`.trim(), 439 formEmail: email?.value?.trim() || '', 440 countryCode: countryCode, 441 formTelefono: phone?.value?.trim() || '', 442 formProvinceState: region?.value?.trim() || '', 443 countryName: countryName, 444 formCheckValue: 0 445 }); 446 447 if (mp?.status === 500) throw new Error(t('Payment rejected', 'Pago rechazado')); 448 resultBox && (resultBox.textContent = t( 449 'Recurring payment completed! Thank you for your support.', 450 '¡Pago recurrente realizado! ¡Gracias por apoyar!' 451 )); 452 postToSheetSimple({ amount: sel.amountUSD, currency: 'USD', recurrence: sel.recurrence }); 453 resetDonationForm() 454 } catch (err) { 455 console.error(err); 456 resultBox && (resultBox.textContent = t( 457 'There was a problem processing the payment. Please try again.', 458 'Ocurrió un problema procesando el pago. Inténtalo de nuevo.' 459 )); 460 } 461 } 462 463 async function startSingle(sel) { 464 const { month, year } = expParts(); 465 const card = (cardNumber?.value || '').replace(/\s+/g,''); 466 const currency = sel.currency; // honor userâs toggle for custom 467 const payAmount = currency === 'CRC' ? Math.round(sel.amountCRC) : sel.amountUSD; 468 469 470 const countrySel = document.getElementById('country'); 471 const countryCode = countrySel?.value || 'CR'; // e.g. "CR" 472 const countryName = countrySel?.selectedOptions?.[0]?.text || 'Costa Rica';// e.g. "Costa Rica" 473 474 try { 475 // 1) single_payment 476 const pre = await wpAjax('single_payment', { 477 amountSelected: payAmount, 478 formName: `${firstName?.value?.trim() || ''} ${lastName?.value?.trim() || ''}`.trim(), 479 formEmail: email?.value?.trim() || '', 480 title: sel.recurrence === 'unica' ? 'Aporte único' : `Suscripción ${sel.re
480currence}`, 481 countryCode: countryCode, 482 currency, 483 formTelefono: phone?.value?.trim() || '', 484 formProvinceState: region?.value?.trim() || '' 485 }); 486 if (pre?.status === 500) throw new Error('Error iniciando pago único'); 487 488 // 2) optional Kount 489 let kountId = null; 490 if (typeof window.GDataCollector !== 'undefined') { 491 await new Promise((resolve) => { 492 let collector = null; 493 window.GDataCollector.init('sand', null, function (res) { 494 collector = res; 495 collector.setupCallback({ 496 'collect-end': function (params) { kountId = params.MercSessId; resolve(); }, 497 'collect-begin': function () {} 498 }); 499 collector.collectData(); 500 }); 501 }); 502 } 503 504 // 3) encrypt card + kount 505 ensureCrypto(); 506 const enc = pack({ 507 card: { 508 cardHolder: cardName.value.trim(), 509 expirationDate: { month, year }, 510 cardNumber: card, 511 cvc: cardCvc.value.trim() 512 }, 513 kountSession: kountId 514 }); 515 516 // 4) send_single_payment 517 const ssp = await wpAjax('send_single_payment', { 518 session: pre.session, 519 token: pre.token, 520 ld: enc.ld, 521 lk: enc.lk, 522 amountSelected: payAmount, 523 formName: `${firstName?.value?.trim() || ''} ${lastName?.value?.trim() || ''}`.trim(), 524 formEmail: email?.value?.trim() || '', 525 title: sel.recurrence === 'unica' ? 'Aporte único' : `Suscripción ${sel.re
525currence}`, 526 countryCode: countryCode, 527 formTelefono: phone?.value?.trim() || '', 528 formProvinceState: region?.value?.trim() || '', 529 countryName: countryName, 530 formCheckValue: 0 531 }); 532 533 if (ssp?.status !== 200) throw new Error(t('One-time payment rejected', 'Pago único rechazado')); 534 resultBox && (resultBox.textContent = t('Payment successful! Thank you!', '¡Pago realizado con éxito! ¡Gracias!')); 535 postToSheetSimple({ amount: payAmount, currency, recurrence: sel.recurrence }); 536 resetDonationForm() 537 } catch (err) { 538 console.error(err); 539 resultBox && (resultBox.textContent = t("Couldn't complete the one-time payment.", 'No se pudo completar el pago único.')); 540 } 541 } 542 543 // ---------- submit handler ---------- 544 form && form.addEventListener('submit', (e) => { 545 e.preventDefault(); 546 // postToSheetSimple(); 547 const sel = currentSelection(); 548 if (!sel) { 549 resultBox && (resultBox.textContent = 'Seleccioná un plan o ingresá un monto.'); 550 return; 551 } 552 resultBox && (resultBox.textContent = t('Processing payment...', 'Procesando pagoâ¦')); 553 554 // Decide flujo: presets -> recurring; custom -> by frequency (default unica) 555 if (!state.customActive) { 556 // preset mensual/anual 557 startRecurring(sel); 558 } else { 559 if ((state.customFreq || 'unica') === 'unica') startSingle(sel); 560 else startRecurring(sel); 561 } 562 }); 563 564 565 // --- Countries (FIRST.org) -> <select id="country"> 566 async function loadCountriesFIRST({ selectId = 'country', defaultISO = 'CR' } = {}) { 567 const sel = document.getElementById(selectId); 568 if (!sel) return; 569 570 try { 571 // Get all countries (FIRST.org defaults to 100; bump to 300 to cover all) 572 const res = await fetch('https://api.first.org/data/v1/countries?limit=300'); 573 const json = await res.json(); 574 const data = json?.data || {}; 575 576 // Turn into array and sort by name (Spanish collation) 577 const entries = Object.entries(data) 578 .map(([iso2, obj]) => ({ code: iso2.toUpperCase(), name: obj.country })) 579 .sort((a, b) => a.name.localeCompare(b.name, 'es')); 580 581 // Reset options 582 sel.innerHTML = '<option value="" disabled selected>Seleccionáâ¦</option>'; 583 584 // Build options 585 const frag = document.createDocumentFragment(); 586 for (const { code, name } of entries) { 587 const opt = document.createElement('option'); 588 opt.value = code; // <-- ISO-2 countryCode youâll send in AJAX 589 opt.textContent = name; // visible label 590 frag.appendChild(opt); 591 } 592 sel.appendChild(frag); 593 594 // Default to Costa Rica if present 595 if (entries.some(e => e.code === defaultISO)) { 596 sel.value = defaultISO; 597 } 598 599 // Fire events so your existing listeners (e.g., validatePersonal) run 600 sel.dispatchEvent(new Event('input', { bubbles: true })); 601 sel.dispatchEvent(new Event('change', { bubbles: true })); 602 } catch (e) { 603 // Fallback minimal list if the API fails 604 sel.innerHTML = ` 605 <option value="" disabled selected>Seleccionáâ¦</option> 606 <option value="CR">Costa Rica</option> 607 <option value="US">United States</option> 608 <option value="MX">Mexico</option> 609 <option value="PA">Panama</option> 610 <option value="ES">Spain</option> 611 `; 612 sel.value = defaultISO; 613 sel.dispatchEvent(new Event('input', { bubbles: true })); 614 sel.dispatchEvent(new Event('change', { bubbles: true })); 615 } 616 } 617 618 setTimeout(function(){ 619 if(document.getElementById('subscribeForm')){ 620 // ---------- init ---------- 621 updateConvertedDisplay(); 622 updatePlanCTA(); 623 validatePersonal(); 624 validatePayment(); 625 setStep(0); 626 loadCountriesFIRST({ selectId: 'country', defaultISO: 'CR' }); 627 } 628 },500) 629})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.