1// ========================================== 2// AUTH CONFIGURATION 3// ========================================== 4const AUTH_CONFIG = { 5 API_BASE_URL: window.location.origin, 6 CSRF_ENDPOINT: '/csrf-cookie', 7 LOGIN_ENDPOINT: '/api/auth/login', 8 REGISTER_ENDPOINT: '/api/auth/register', 9 PLANS_ENDPOINT: '/api/subscription/plans', 10 SUBSCRIBE_ENDPOINT: '/api/subscription/create', 11 SUBSCRIBE_CONFIRM_ENDPOINT: '/api/subscription/confirm', 12 REDIRECT_URL: '/', 13}; 14 15// ========================================== 16// STRIPE CONFIGURATION 17// ========================================== 18// Publishable key - this should match your environment 19// For production, this will be loaded from your backend or set via environment 20const STRIPE_CONFIG = { 21 PUBLISHABLE_KEY: 'pk_test_51SG2QaCmQ5B6YIyRexakJaN74zeXPXrnblqXEGpFoozcTbWa0dsB0OENcfk2bLOZoOkykeCKlaRZui79ldELUP0Z0047Gdq7Qg', 22 // Fallback prices (will be overridden by API response) 23 PRICES: { 24 discover: 'price_1SG49PCmQ5B6YIyRWtT6DqN6', 25 merchant: 'price_1SG4DQCmQ5B6YIyRoXSI6JUl', 26 pro: 'price_1SG4EJCmQ5B6YIyRS04XDYkV', 27 business: 'price_1SG4EjCmQ5B6YIyRHd45wgMZ' 28 } 29}; 30 31let cachedCsrfToken = null; 32let stripe = null; 33let cardElement = null; 34let selectedPlan = null; 35let selectedPriceId = null; 36let registeredUserData = null; 37let availablePlans = []; 38 39// ========================================== 40// CSRF TOKEN HANDLING 41// ========================================== 42function getCsrfTokenFromCookie() { 43 const cookies = document.cookie.split(';'); 44 for (let cookie of cookies) { 45 const [name, value] = cookie.trim().split('='); 46 if (name === 'XSRF-TOKEN') { 47 return decodeURIComponent(value); 48 } 49 } 50 return null; 51} 52 53async function fetchCsrfToken() { 54 if (cachedCsrfToken) { 55 return cachedCsrfToken; 56 } 57 58 try { 59 const response = await fetch(`${AUTH_CONFIG.API_BASE_URL}${AUTH_CONFIG.CSRF_ENDPOINT}`, { 60 method: 'GET', 61 credentials: 'include', 62 headers: { 63 'Accept': 'application/json', 64 'X-Requested-With': 'XMLHttpRequest' 65 } 66 }); 67 68 await new Promise(resolve => setTimeout(resolve, 100)); 69 const token = getCsrfTokenFromCookie(); 70 71 if (token) { 72 cachedCsrfToken = token; 73 return token; 74 } 75 76 const xsrfToken = response.headers.get('X-XSRF-TOKEN'); 77 if (xsrfToken) { 78 cachedCsrfToken = xsrfToken; 79 return xsrfToken; 80 } 81 82 return ''; 83 } catch (error) { 84 console.error('Failed to fetch CSRF token:', error); 85 return ''; 86 } 87} 88 89// ========================================== 90// AUTH MODAL FUNCTIONS 91// ========================================== 92function openAuthModal(type) { 93 // Registration modal may be disabled/removed from the markup. 94 if (type !== 'login' && !document.getElementById('registerModal')) { 95 type = 'login'; 96 } 97 98 const modalId = type === 'login' ? 'loginModal' : 'registerModal'; 99 const modalEl = document.getElementById(modalId); 100 if (!modalEl) return; 101 102 modalEl.classList.add('active'); 103 document.body.style.overflow = 'hidden'; 104 fetchCsrfToken(); 105} 106 107function closeAuthModal(type) { 108 if (type !== 'login' && !document.getElementById('registerModal')) { 109 type = 'login'; 110 } 111 112 const modalId = type === 'login' ? 'loginModal' : 'registerModal'; 113 const modalEl = document.getElementById(modalId); 114 if (!modalEl) return; 115 116 modalEl.classList.remove('active'); 117 document.body.style.overflow = ''; 118 clearAuthErrors(type); 119} 120 121function switchAuthModal(toType) { 122 if (toType !== 'login' && !document.getElementById('registerModal')) { 123 toType = 'login'; 124 } 125 126 const fromType = toType === 'login' ? 'register' : 'login'; 127 closeAuthModal(fromType); 128 setTimeout(() => openAuthModal(toType), 150); 129} 130 131// Expose auth functions to global scope for onclick handlers 132window.openAuthModal = openAuthModal; 133window.closeAuthModal = closeAuthModal; 134window.switchAuthModal = switchAuthModal; 135 136// Close modal on overlay click 137document.querySelectorAll('.auth-modal-overlay').forEach(overlay => { 138 overlay.addEventListener('click', function(e) { 139 if (e.target === this) { 140 if (this.id === 'paymentModal') { 141 closePaymentModal(); 142 return; 143 } 144 const type = this.id === 'loginModal' ? 'login' : 'register'; 145 closeAuthModal(type); 146 } 147 }); 148}); 149 150// Close modal with Escape key 151document.addEventListener('keydown', (e) => { 152 if (e.key === 'Escape') { 153 const loginModal = document.getElementById('loginModal'); 154 if (loginModal && loginModal.classList.contains('active')) { 155 closeAuthModal('login'); 156 } 157 const registerModal = document.getElementById('registerModal'); 158 if (registerModal && registerModal.classList.contains('active')) { 159 closeAuthModal('register'); 160 } 161 const paymentModal = document.getElementById('paymentModal'); 162 if (paymentModal && paymentModal.classList.contains('active')) { 163 closePaymentModal(); 164 } 165 closeMenu(); 166 } 167}); 168 169// ========================================== 170// AUTH FORM HELPERS 171// ========================================== 172function clearAuthErrors(type) { 173 const prefix = type === 'login' ? 'login' : 'register'; 174 document.querySelectorAll(`#${prefix}Modal .auth-error-message`).forEach(el => { 175 el.textContent = ''; 176 el.classList.remove('show'); 177 }); 178 const alert = document.getElementById(`${prefix}ErrorAlert`); 179 alert.textContent = ''; 180 alert.classList.remove('show'); 181} 182 183function showAuthFieldError(type, fieldName, message) { 184 const errorEl = document.getElementById(`${type}_${fieldName}_error`); 185 if (errorEl) { 186 errorEl.textContent = message; 187 errorEl.classList.add('show'); 188 } 189} 190 191function showAuthAlert(type, message) { 192 const alert = document.getElementById(`${type}ErrorAlert`); 193 alert.textContent = message; 194 alert.classList.add('show'); 195} 196 197function setAuthFormDisabled(type, disabled) { 198 const formId = type === 'login' ? 'loginForm' : 'registerForm';
199 const btnId = type === 'login' ? 'loginSubmitBtn' : 'registerSubmitBtn'; 200 const form = document.getElementById(formId); 201 const inputs = form.querySelectorAll('input, button'); 202 inputs.forEach(input => input.disabled = disabled); 203 204 const btn = document.getElementById(btnId); 205 if (disabled) { 206 btn.classList.add('loading'); 207 } else { 208 btn.classList.remove('loading'); 209 } 210} 211 212// ========================================== 213// LOGIN FORM HANDLER 214// ========================================== 215document.getElementById('loginForm').addEventListener('submit', async function(e) { 216 e.preventDefault(); 217 clearAuthErrors('login'); 218 setAuthFormDisabled('login', true); 219 220 const submitData = { 221 email: document.getElementById('loginEmail').value.trim(), 222 password: document.getElementById('loginPassword').value, 223 remember: document.getElementById('loginRemember').checked, 224 redirect_url: AUTH_CONFIG.REDIRECT_URL 225 }; 226 227 try { 228 const csrfToken = await fetchCsrfToken(); 229 230 const response = await fetch(`${AUTH_CONFIG.API_BASE_URL}${AUTH_CONFIG.LOGIN_ENDPOINT}`, { 231 method: 'POST', 232 headers: { 233 'Content-Type': 'application/json', 234 'Accept': 'application/json', 235 'X-CSRF-TOKEN': csrfToken, 236 'X-Requested-With': 'XMLHttpRequest' 237 }, 238 credentials: 'include', 239 body: JSON.stringify(submitData) 240 }); 241 242 const data = await response.json(); 243 244 if (response.ok && data.success) { 245 window.location.href = data.redirect_url; 246 } else { 247 if (data.errors) { 248 for (const [field, messages] of Object.entries(data.errors)) { 249 if (Array.isArray(messages) && messages.length > 0) { 250 showAuthFieldError('login', field, messages[0]); 251 } 252 } 253 } 254 if (data.message) { 255 showAuthAlert('login', data.message); 256 } else { 257 showAuthAlert('login', 'Login failed. Please check your credentials and try again.'); 258 } 259 setAuthFormDisabled('login', false); 260 } 261 } catch (error) { 262 console.error('Login error:', error); 263 showAuthAlert('login', 'An error occurred. Please try again.'); 264 setAuthFormDisabled('login', false); 265 } 266}); 267 268// ========================================== 269// REGISTER FORM HANDLER 270// ========================================== 271const registerFormEl = document.getElementById('registerForm'); 272if (registerFormEl) registerFormEl.addEventListener('submit', async function(e) { 273 e.preventDefault(); 274 clearAuthErrors('register'); 275 setAuthFormDisabled('register', true); 276 277 const formData = { 278 first_name: document.getElementById('registerFirstName').value.trim(), 279 last_name: document.getElementById('registerLastName').value.trim(), 280 email: document.getElementById('registerEmail').value.trim(), 281 password: document.getElementById('registerPassword').value, 282 password_confirmation: document.getElementById('registerPasswordConfirm').value, 283 }; 284 285 const submitData = { 286 name: `${formData.first_name} ${formData.last_name}`.trim(), 287 email: formData.email, 288 password: formData.password, 289 password_confirmation: formData.password_confirmation, 290 redirect_url: AUTH_CONFIG.REDIRECT_URL 291 }; 292 293 try { 294 const csrfToken = await fetchCsrfToken(); 295 296 const response = await fetch(`${AUTH_CONFIG.API_BASE_URL}${AUTH_CONFIG.REGISTER_ENDPOINT}`, { 297 method: 'POST', 298 headers: { 299 'Content-Type': 'application/json', 300 'Accept': 'application/json', 301 'X-CSRF-TOKEN': csrfToken, 302 'X-Requested-With': 'XMLHttpRequest' 303 }, 304 credentials: 'include', 305 body: JSON.stringify(submitData) 306 }); 307 308 const data = await response.json(); 309 310 if (response.ok && data.success) { 311 // Store user data for payment 312 registeredUserData = { 313 ...data, 314 email: formData.email 315 }; 316 317 // Check if we're in the two-step modal 318 const step2 = document.getElementById('registerStep2'); 319 if (step2) { 320 // Activate step 2 in the same modal 321 activateModalStep2(); 322 } else { 323 // Fallback to separate payment modal 324 closeAuthModal('register'); 325 openPaymentModal(); 326 } 327 } else { 328 if (data.errors) { 329 for (const [field, messages] of Object.entries(data.errors)) { 330 if (Array.isArray(messages) && messages.length > 0) { 331 showAuthFieldError('register', field, messages[0]); 332 } 333 } 334 } 335 if (data.message) { 336 showAuthAlert('register', data.message); 337 } else { 338 showAuthAlert('register', 'Registration failed. Please check your information and try again.'); 339 } 340 setAuthFormDisabled('register', false); 341 } 342 } catch (error) { 343 console.error('Registration error:', error); 344 showAuthAlert('register', 'An error occurred. Please try again.'); 345 setAuthFormDisabled('register', false); 346 } 347}); 348 349// ========================================== 350// STRIPE & PAYMENT HANDLING 351// ========================================== 352async function loadPlansFromAPI() { 353 try { 354 const response = await fetch(`${AUTH_CONFIG.API_BASE_URL}${AUTH_CONFIG.PLANS_ENDPOINT}`, { 355 method: 'GET', 356 headers: { 357 'Accept': 'application/json', 358 }, 359 credentials: 'include' 360 }); 361 362 const data = await response.json(); 363 364 if (data.success && data.plans) { 365 availablePlans = data.plans; 366 367 // Build PRICES map from API response
368 data.plans.forEach(plan => { 369 STRIPE_CONFIG.PRICES[plan.name.toLowerCase()] = plan.price_id; 370 }); 371 372 // Update trial badge if trial days provided 373 if (data.trial_days > 0) { 374 const trialBadge = document.querySelector('.trial-badge'); 375 if (trialBadge) { 376 trialBadge.innerHTML = `ð ${data.trial_days} Days Free â Cancel anytime, no charge`; 377 } 378 } 379 380 updatePlanOptionsFromAPI(data.plans); 381 } 382 } catch (error) { 383 console.error('Failed to load plans:', error); 384 // Keep default hardcoded plans as fallback 385 } 386} 387 388function updatePlanOptionsFromAPI(plans) { 389 const planContainer = document.querySelector('.plan-selection'); 390 if (!planContainer || !plans.length) return; 391 392 // Clear existing plans 393 planContainer.innerHTML = ''; 394 395 // Create plan options from API data 396 plans.forEach((plan, index) => { 397 const isMiddle = index === 1; // Select middle plan by default 398 const option = document.createElement('div'); 399 option.className = `plan-option${isMiddle ? ' selected' : ''}`; 400 option.dataset.plan = plan.name.toLowerCase(); 401 option.dataset.priceId = plan.price_id; 402 403 option.innerHTML = ` 404 <div class="plan-price">$${Math.round(plan.price)}<span>/mo</span></div> 405 <div class="plan-title">${plan.name}</div> 406 <div class="plan-features">${plan.prospecting_scans} scans · ${plan.listings} listings</div> 407 `; 408 409 option.addEventListener('click', function() { 410 document.querySelectorAll('.plan-option').forEach(opt => opt.classList.remove('selected')); 411 this.classList.add('selected'); 412 selectedPlan = this.dataset.plan; 413 selectedPriceId = this.dataset.priceId; 414 }); 415 416 planContainer.appendChild(option); 417 418 // Set default selection 419 if (isMiddle) { 420 selectedPlan = plan.name.toLowerCase(); 421 selectedPriceId = plan.price_id; 422 } 423 }); 424} 425 426function initializeStripe() { 427 if (stripe) return; // Already initialized 428 429 stripe = Stripe(STRIPE_CONFIG.PUBLISHABLE_KEY); 430 const elements = stripe.elements({ 431 fonts: [ 432 { cssSrc: 'https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600&display=swap' } 433 ] 434 }); 435 436 const style = { 437 base: { 438 color: '#ffffff', 439 fontFamily: 'Montserrat, sans-serif', 440 fontSmoothing: 'antialiased', 441 fontSize: '16px', 442 '::placeholder': { 443 color: 'rgba(255, 255, 255, 0.4)' 444 }, 445 iconColor: '#ffffff' 446 }, 447 invalid: { 448 color: '#ff4757', 449 iconColor: '#ff4757' 450 } 451 }; 452 453 cardElement = elements.create('card', { 454 style: style, 455 hidePostalCode: false 456 }); 457 458 cardElement.mount('#card-element'); 459 460 cardElement.on('change', function(event) { 461 const displayError = document.getElementById('card-errors'); 462 const container = document.getElementById('card-element-container'); 463 464 if (event.error) { 465 displayError.textContent = event.error.message; 466 container.classList.add('error'); 467 } else { 468 displayError.textContent = ''; 469 container.classList.remove('error'); 470 } 471 }); 472} 473 474async function openPaymentModal() { 475 document.getElementById('paymentModal').classList.add('active'); 476 document.body.style.overflow = 'hidden'; 477 478 // Reset to form view 479 document.getElementById('paymentFormContainer').style.display = 'block'; 480 document.getElementById('paymentSuccessContainer').style.display = 'none'; 481 482 // Load plans from API if not already loaded 483 if (availablePlans.length === 0) { 484 await loadPlansFromAPI(); 485 } 486 487 // Initialize Stripe if not already done 488 setTimeout(initializeStripe, 100); 489} 490 491function closePaymentModal() { 492 document.getElementById('paymentModal').classList.remove('active'); 493 document.body.style.overflow = ''; 494} 495 496// Plan selection handling (for hardcoded fallback plans) 497document.querySelectorAll('.plan-option').forEach(option => { 498 option.addEventListener('click', function() { 499 document.querySelectorAll('.plan-option').forEach(opt => opt.classList.remove('selected')); 500 this.classList.add('selected'); 501 selectedPlan = this.dataset.plan; 502 selectedPriceId = this.dataset.priceId || STRIPE_CONFIG.PRICES[selectedPlan]; 503 }); 504}); 505 506// Set initial selection from hardcoded plans 507const initialSelected = document.querySelector('.plan-option.selected'); 508if (initialSelected) { 509 selectedPlan = initialSelected.dataset.plan; 510 selectedPriceId = initialSelected.dataset.priceId; 511} 512 513async function handlePaymentSubmit() { 514 const submitBtn = document.getElementById('paymentSubmitBtn'); 515 submitBtn.classList.add('loading'); 516 submitBtn.disabled = true; 517 518 const errorElement = document.getElementById('card-errors'); 519 errorElement.textContent = ''; 520 521 // Ensure we have a plan selected 522 const priceId = selectedPriceId || STRIPE_CONFIG.PRICES[selectedPlan]; 523 if (!priceId) { 524 errorElement.textContent = 'Please select a plan.'; 525 submitBtn.classList.remove('loading'); 526 submitBtn.disabled = false;
527 return; 528 } 529 530 try { 531 // Create payment method with Stripe 532 const { paymentMethod, error } = await stripe.createPaymentMethod({ 533 type: 'card', 534 card: cardElement, 535 }); 536 537 if (error) { 538 errorElement.textContent = error.message; 539 submitBtn.classList.remove('loading'); 540 submitBtn.disabled = false; 541 return; 542 } 543 544 // Send to backend to create subscription 545 const csrfToken = await fetchCsrfToken(); 546 547 const response = await fetch(`${AUTH_CONFIG.API_BASE_URL}${AUTH_CONFIG.SUBSCRIBE_ENDPOINT}`, { 548 method: 'POST', 549 headers: { 550 'Content-Type': 'application/json', 551 'Accept': 'application/json', 552 'X-CSRF-TOKEN': csrfToken, 553 'X-Requested-With': 'XMLHttpRequest' 554 }, 555 credentials: 'include', 556 body: JSON.stringify({ 557 payment_method_id: paymentMethod.id, 558 price_id: priceId, 559 plan: selectedPlan 560 }) 561 }); 562 563 const data = await response.json(); 564 565 if (data.requires_action) { 566 // Handle 3D Secure authentication 567 const { error: confirmError, paymentIntent } = await stripe.confirmCardPayment( 568 data.client_secret 569 ); 570 571 if (confirmError) { 572 errorElement.textContent = confirmError.message; 573 submitBtn.classList.remove('loading'); 574 submitBtn.disabled = false; 575 return; 576 } 577 578 // Confirm the subscription after 3DS 579 const confirmResponse = await fetch(`${AUTH_CONFIG.API_BASE_URL}${AUTH_CONFIG.SUBSCRIBE_CONFIRM_ENDPOINT}`, { 580 method: 'POST', 581 headers: { 582 'Content-Type': 'application/json', 583 'Accept': 'application/json', 584 'X-CSRF-TOKEN': csrfToken, 585 'X-Requested-With': 'XMLHttpRequest' 586 }, 587 credentials: 'include', 588 body: JSON.stringify({ 589 payment_intent_id: paymentIntent.id 590 }) 591 }); 592 593 const confirmData = await confirmResponse.json(); 594 595 if (confirmData.success) { 596 showPaymentSuccess(); 597 } else { 598 errorElement.textContent = confirmData.message || 'Subscription failed. Please try again.'; 599 submitBtn.classList.remove('loading'); 600 submitBtn.disabled = false; 601 } 602 } else if (data.success) { 603 showPaymentSuccess(); 604 } else { 605 errorElement.textContent = data.message || 'Subscription failed. Please try again.'; 606 submitBtn.classList.remove('loading'); 607 submitBtn.disabled = false; 608 } 609 } catch (error) { 610 console.error('Payment error:', error); 611 errorElement.textContent = 'An error occurred. Please try again.'; 612 submitBtn.classList.remove('loading'); 613 submitBtn.disabled = false; 614 } 615} 616 617function showPaymentSuccess() { 618 document.getElementById('paymentFormContainer').style.display = 'none'; 619 document.getElementById('paymentSuccessContainer').style.display = 'block'; 620} 621 622function skipPayment() { 623 // User chose to skip payment - redirect to app 624 redirectToApp(); 625} 626 627function redirectToApp() { 628 window.location.href = registeredUserData?.redirect_url || AUTH_CONFIG.REDIRECT_URL; 629} 630 631// Expose payment functions to global scope for onclick handlers 632window.closePaymentModal = closePaymentModal; 633window.handlePaymentSubmit = handlePaymentSubmit; 634window.skipPayment = skipPayment; 635window.redirectToApp = redirectToApp; 636 637// Close payment modal on overlay click 638document.getElementById('paymentModal').addEventListener('click', function(e) { 639 if (e.target === this) { 640 closePaymentModal(); 641 } 642}); 643 644// Close payment modal on escape key 645document.addEventListener('keydown', function(e) { 646 if (e.key === 'Escape' && document.getElementById('paymentModal').classList.contains('active')) { 647 closePaymentModal(); 648 } 649}); 650 651// ========================================== 652// INLINE SIGNUP FORM HANDLING 653// ========================================== 654let inlineCardElement = null; 655let inlineStripe = null; 656let inlineSelectedPlan = 'merchant'; 657let inlineSelectedPriceId = 'price_1SG4DQCmQ5B6YIyRoXSI6JUl'; 658let inlineRegisteredUserData = null; 659 660// Initialize inline plan selection 661document.querySelectorAll('.inline-plan-option').forEach(option => { 662 option.addEventListener('click', function() { 663 document.querySelectorAll('.inline-plan-option').forEach(opt => opt.classList.remove('selected')); 664 this.classList.add('selected'); 665 inlineSelectedPlan = this.dataset.plan; 666 inlineSelectedPriceId = this.dataset.priceId; 667 }); 668}); 669 670// Set initial inline plan selection 671const initialInlineSelected = document.querySelector('.inline-plan-option.selected'); 672if (initialInlineSelected) { 673 inlineSelectedPlan = initialInlineSelected.dataset.plan;
674 inlineSelectedPriceId = initialInlineSelected.dataset.priceId; 675} 676 677function initializeInlineStripe() { 678 if (inlineStripe && inlineCardElement) return; 679 680 inlineStripe = Stripe(STRIPE_CONFIG.PUBLISHABLE_KEY); 681 const elements = inlineStripe.elements({ 682 fonts: [ 683 { cssSrc: 'https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600&display=swap' } 684 ] 685 }); 686 687 const style = { 688 base: { 689 color: '#ffffff', 690 fontFamily: 'Montserrat, sans-serif', 691 fontSmoothing: 'antialiased', 692 fontSize: '16px', 693 '::placeholder': { 694 color: 'rgba(255, 255, 255, 0.4)' 695 }, 696 iconColor: '#ffffff' 697 }, 698 invalid: { 699 color: '#ff4757', 700 iconColor: '#ff4757' 701 } 702 }; 703 704 inlineCardElement = elements.create('card', { 705 style: style, 706 hidePostalCode: false 707 }); 708 709 inlineCardElement.mount('#inline-card-element'); 710 711 inlineCardElement.on('change', function(event) { 712 const displayError = document.getElementById('inline-card-errors'); 713 const container = document.getElementById('inline-card-element-container'); 714 715 if (event.error) { 716 displayError.textContent = event.error.message; 717 container.classList.add('error'); 718 } else { 719 displayError.textContent = ''; 720 container.classList.remove('error'); 721 } 722 }); 723} 724 725function clearInlineErrors() { 726 document.querySelectorAll('#inlineSignupCard .inline-error-message').forEach(el => { 727 el.textContent = ''; 728 }); 729 const alert = document.getElementById('inlineSignupAlert'); 730 alert.textContent = ''; 731 alert.classList.remove('visible'); 732} 733 734function showInlineFieldError(fieldName, message) { 735 const errorEl = document.getElementById(`inline_${fieldName}_error`); 736 if (errorEl) { 737 errorEl.textContent = message; 738 } 739} 740 741function showInlineAlert(elementId, message, isSuccess = false) { 742 const alert = document.getElementById(elementId); 743 alert.textContent = message; 744 alert.classList.add('visible'); 745 if (isSuccess) { 746 alert.classList.add('inline-success-alert'); 747 } else { 748 alert.classList.remove('inline-success-alert'); 749 } 750} 751 752function setInlineFormDisabled(formId, btnId, disabled) { 753 const form = document.getElementById(formId); 754 const inputs = form.querySelectorAll('input, button'); 755 inputs.forEach(input => input.disabled = disabled); 756 757 const btn = document.getElementById(btnId); 758 if (disabled) { 759 btn.classList.add('loading'); 760 } else { 761 btn.classList.remove('loading'); 762 } 763} 764 765// Inline signup form handler (optional; may be disabled/removed from markup) 766const inlineSignupForm = document.getElementById('inlineSignupForm'); 767if (inlineSignupForm) inlineSignupForm.addEventListener('submit', async function(e) { 768 e.preventDefault(); 769 clearInlineErrors(); 770 setInlineFormDisabled('inlineSignupForm', 'inlineSignupBtn', true); 771 772 const formData = { 773 first_name: document.getElementById('inlineFirstName').value.trim(), 774 last_name: document.getElementById('inlineLastName').value.trim(), 775 email: document.getElementById('inlineEmail').value.trim(), 776 password: document.getElementById('inlinePassword').value, 777 password_confirmation: document.getElementById('inlinePasswordConfirm').value, 778 }; 779 780 const submitData = { 781 name: `${formData.first_name} ${formData.last_name}`.trim(), 782 email: formData.email, 783 password: formData.password, 784 password_confirmation: formData.password_confirmation, 785 redirect_url: AUTH_CONFIG.REDIRECT_URL 786 }; 787 788 try { 789 const csrfToken = await fetchCsrfToken(); 790 791 const response = await fetch(`${AUTH_CONFIG.API_BASE_URL}${AUTH_CONFIG.REGISTER_ENDPOINT}`, { 792 method: 'POST', 793 headers: { 794 'Content-Type': 'application/json', 795 'Accept': 'application/json', 796 'X-CSRF-TOKEN': csrfToken, 797 'X-Requested-With': 'XMLHttpRequest' 798 }, 799 credentials: 'include', 800 body: JSON.stringify(submitData) 801 }); 802 803 const data = await response.json(); 804 805 if (response.ok && data.success) { 806 // Inline signup stays on .com (payment step) â fire here so the .com pixel cookie attributes. 807 try { 808 if (window.GlobalSkuMeta) { 809 window.GlobalSkuMeta.completeRegistration({ content_name: 'account_created', status: true }); 810 } 811 } catch (e) {} 812 813 // Store user data 814 inlineRegisteredUserData = { 815 ...data, 816 email: formData.email 817 }; 818 819 // Show success state on signup card 820 document.getElementById('inlineSignupFormContent').classList.add('hidden'); 821 document.getElementById('inlineSignupSuccess').classList.add('visible'); 822 823 // Enable payment card 824 const paymentCard = document.getElementById('inlinePaymentCard'); 825 paymentCard.classList.remove('disabled'); 826 paymentCard.classList.add('active'); 827 828 // Initialize Stripe for payment 829 setTimeout(initializeInlineStripe, 100); 830 831 } else { 832 if (data.errors) { 833 for (const [field, messages] of Object.entries(data.errors)) { 834 if (Array.isArray(messages) && messages.length > 0) { 835 showInlineFieldError(field, messages[0]); 836 } 837 } 838 } 839 if (data.message) { 840 showInlineAlert('inlineSignupAlert', data.message); 841 } else { 842 showInlineAlert('inlineSignupAlert', 'Registration failed. Please check your information and try again.'); 843 } 844 setInlineFormDisabled('inlineSignupForm', 'inlineSignupBtn', false); 845 } 846 } catch (error) { 847 console.error('Inline registration error:', error); 848 showInlineAlert('inlineSignupAlert', 'An error occurred. Please try again.'); 849 setInlineFormDisabled('inlineSignupForm', 'inlineSignupBtn', false); 850 } 851}); 852 853async function handleInlinePaymentSubmit() { 854 const submitBtn = document.getElementById('inlinePaymentBtn'); 855 submitBtn.classList.add('loading'); 856 submitBtn.disabled = true; 857 858 const errorElement = document.getElementById('inline-card-errors'); 859 errorElement.textContent = ''; 860 861 const priceId = inlineSelectedPriceId || STRIPE_CONFIG.PRICES[inlineSelectedPlan]; 862 if (!priceId) { 863 errorElement.textContent = 'Please select a plan.'; 864 submitBtn.classList.remove('loading'); 865 submitBtn.disabled = false;
866 return; 867 } 868 869 try { 870 try { 871 if (window.GlobalSkuMeta) { 872 window.GlobalSkuMeta.initiateCheckout({ currency: 'USD' }); 873 } 874 } catch (e) {} 875 876 const { paymentMethod, error } = await inlineStripe.createPaymentMethod({ 877 type: 'card', 878 card: inlineCardElement, 879 }); 880 881 if (error) { 882 errorElement.textContent = error.message; 883 submitBtn.classList.remove('loading'); 884 submitBtn.disabled = false; 885 return; 886 } 887 888 const csrfToken = await fetchCsrfToken(); 889 890 const response = await fetch(`${AUTH_CONFIG.API_BASE_URL}${AUTH_CONFIG.SUBSCRIBE_ENDPOINT}`, { 891 method: 'POST', 892 headers: { 893 'Content-Type': 'application/json', 894 'Accept': 'application/json', 895 'X-CSRF-TOKEN': csrfToken, 896 'X-Requested-With': 'XMLHttpRequest' 897 }, 898 credentials: 'include', 899 body: JSON.stringify({ 900 payment_method_id: paymentMethod.id, 901 price_id: priceId, 902 plan: inlineSelectedPlan 903 }) 904 }); 905 906 const data = await response.json(); 907 908 if (data.requires_action) { 909 const { error: confirmError, paymentIntent } = await inlineStripe.confirmCardPayment( 910 data.client_secret 911 ); 912 913 if (confirmError) { 914 errorElement.textContent = confirmError.message; 915 submitBtn.classList.remove('loading'); 916 submitBtn.disabled = false; 917 return; 918 } 919 920 const confirmResponse = await fetch(`${AUTH_CONFIG.API_BASE_URL}${AUTH_CONFIG.SUBSCRIBE_CONFIRM_ENDPOINT}`, { 921 method: 'POST', 922 headers: { 923 'Content-Type': 'application/json', 924 'Accept': 'application/json', 925 'X-CSRF-TOKEN': csrfToken, 926 'X-Requested-With': 'XMLHttpRequest' 927 }, 928 credentials: 'include', 929 body: JSON.stringify({ 930 payment_intent_id: paymentIntent.id 931 }) 932 }); 933 934 const confirmData = await confirmResponse.json(); 935 936 if (confirmData.success) { 937 showInlinePaymentSuccess(); 938 } else { 939 errorElement.textContent = confirmData.message || 'Subscription failed. Please try again.'; 940 submitBtn.classList.remove('loading'); 941 submitBtn.disabled = false; 942 } 943 } else if (data.success) { 944 showInlinePaymentSuccess(); 945 } else { 946 errorElement.textContent = data.message || 'Subscription failed. Please try again.'; 947 submitBtn.classList.remove('loading'); 948 submitBtn.disabled = false; 949 } 950 } catch (error) { 951 console.error('Inline payment error:', error); 952 errorElement.textContent = 'An error occurred. Please try again.'; 953 submitBtn.classList.remove('loading'); 954 submitBtn.disabled = false; 955 } 956} 957 958function showInlinePaymentSuccess() { 959 try { 960 if (window.GlobalSkuMeta) { 961 // Landing inline subscribe typically starts a trial; CAPI covers paid conversion later. 962 window.GlobalSkuMeta.startTrial({ value: 0, currency: 'USD' }); 963 } 964 } catch (e) {} 965 document.getElementById('inlinePaymentFormContent').classList.add('hidden'); 966 document.getElementById('inlinePaymentSuccess').classList.add('visible'); 967} 968 969function skipInlinePayment() { 970 redirectToApp(); 971} 972 973// ========================================== 974// NAVIGATION AND MENU 975// ========================================== 976// Navigation dots click handler 977document.querySelectorAll('.nav-dot').forEach(dot => { 978 dot.addEventListener('click', function() { 979 const targetId = this.getAttribute('data-target'); 980 document.getElementById(targetId).scrollIntoView({ behavior: 'smooth' }); 981 }); 982}); 983 984// Update active nav dot on scroll 985const sections = document.querySelectorAll('section'); 986const navDots = document.querySelectorAll('.nav-dot'); 987 988const observerOptions = { 989 root: null, 990 rootMargin: '0px', 991 threshold: 0.5 992}; 993 994const observer = new IntersectionObserver((entries) => { 995 entries.forEach(entry => { 996 if (entry.isIntersecting) { 997 const targetId = entry.target.id; 998 navDots.forEach(dot => { 999 dot.classList.remove('active'); 1000 if (dot.getAttribute('data-target') === targetId) { 1001 dot.classList.add('active'); 1002 } 1003 }); 1004 } 1005 }); 1006}, observerOptions); 1007 1008sections.forEach(section => observer.observe(section)); 1009 1010// ========================================== 1011// MODAL TWO-STEP PAYMENT FLOW 1012// ========================================== 1013let modalStripe = null; 1014let modalCardElement = null; 1015
1016function activateModalStep2() { 1017 const step1 = document.getElementById('registerStep1'); 1018 const step2 = document.getElementById('registerStep2'); 1019 const step2Pending = document.getElementById('registerStep2Pending'); 1020 const step2Active = document.getElementById('registerStep2Active'); 1021 1022 if (!step2) return; 1023 1024 // Check if mobile (single column layout) 1025 const isMobile = window.innerWidth <= 900; 1026 1027 if (isMobile) { 1028 // On mobile: hide Step 1 completely, show Step 2 1029 step1.classList.add('auth-step-completed'); 1030 step1.classList.remove('auth-step-active'); 1031 step2.classList.add('auth-step-active'); 1032 step2.classList.remove('auth-step-pending'); 1033 } else { 1034 // On desktop: fade Step 1, activate Step 2 1035 step1.classList.remove('auth-step-active'); 1036 step1.style.opacity = '0.6'; 1037 step1.style.pointerEvents = 'none'; 1038 } 1039 1040 // Update step 1 badge to show checkmark 1041 const step1Badge = step1.querySelector('.auth-step-badge'); 1042 if (step1Badge) { 1043 step1Badge.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg> Complete'; 1044 step1Badge.style.background = 'rgba(1, 166, 133, 0.2)'; 1045 step1Badge.style.color = '#01a685'; 1046 } 1047 1048 // Activate step 2 1049 step2.classList.remove('auth-step-pending'); 1050 step2.classList.add('auth-step-active'); 1051 1052 const step2Badge = step2.querySelector('.auth-step-badge'); 1053 if (step2Badge) { 1054 step2Badge.classList.remove('auth-step-badge-pending'); 1055 } 1056 1057 // Show active content, hide pending 1058 if (step2Pending) step2Pending.style.display = 'none'; 1059 if (step2Active) step2Active.style.display = 'block'; 1060 1061 // Scroll to top of modal on mobile 1062 if (isMobile) { 1063 const modal = document.getElementById('registerModal'); 1064 if (modal) { 1065 modal.scrollTop = 0; 1066 const modalContent = modal.querySelector('.auth-modal-wide'); 1067 if (modalContent) modalContent.scrollTop = 0; 1068 } 1069 } 1070 1071 // Initialize Stripe for modal 1072 initModalStripe(); 1073 1074 // Setup plan selection for modal 1075 setupModalPlanSelection(); 1076} 1077 1078function initModalStripe() { 1079 if (modalStripe) return; // Already initialized 1080 1081 try { 1082 modalStripe = Stripe(window.STRIPE_CONFIG?.PUBLISHABLE_KEY || STRIPE_CONFIG.PUBLISHABLE_KEY); 1083 const elements = modalStripe.elements(); 1084 1085 modalCardElement = elements.create('card', { 1086 style: { 1087 base: { 1088 color: '#ffffff', 1089 fontFamily: 'Cambay, sans-serif', 1090 fontSize: '16px', 1091 '::placeholder': { color: 'rgba(255, 255, 255, 0.4)' } 1092 }, 1093 invalid: { color: '#ff6b6b' } 1094 } 1095 }); 1096 1097 const container = document.getElementById('modal-card-element'); 1098 if (container) { 1099 modalCardElement.mount('#modal-card-element'); 1100 1101 modalCardElement.on('change', function(event) { 1102 const errorEl = document.getElementById('modal-card-errors'); 1103 if (errorEl) { 1104 errorEl.textContent = event.error ? event.error.message : ''; 1105 } 1106 }); 1107 } 1108 } catch (error) { 1109 console.error('Failed to initialize Stripe for modal:', error); 1110 } 1111} 1112 1113function setupModalPlanSelection() { 1114 const planOptions = document.querySelectorAll('.plan-option-inline'); 1115 planOptions.forEach(option => { 1116 option.addEventListener('click', function() { 1117 planOptions.forEach(p => p.classList.remove('selected')); 1118 this.classList.add('selected'); 1119 }); 1120 }); 1121} 1122 1123async function handleModalPaymentSubmit() { 1124 const btn = document.getElementById('modalPaymentSubmitBtn'); 1125 if (!btn) return; 1126 1127 btn.disabled = true; 1128 btn.classList.add('loading'); 1129 1130 const selectedPlan = document.querySelector('.plan-option-inline.selected'); 1131 const priceId = selectedPlan?.dataset.priceId; 1132 1133 if (!priceId) { 1134 showModalPaymentError('Please select a plan'); 1135 btn.disabled = false; 1136 btn.classList.remove('loading'); 1137 return; 1138 } 1139 1140 if (!modalCardElement) { 1141 showModalPaymentError('Payment form not ready. Please try again.'); 1142 btn.disabled = false;
1143 btn.classList.remove('loading'); 1144 return; 1145 } 1146 1147 try { 1148 const { paymentMethod, error } = await modalStripe.createPaymentMethod({ 1149 type: 'card', 1150 card: modalCardElement 1151 }); 1152 1153 if (error) { 1154 showModalPaymentError(error.message); 1155 btn.disabled = false; 1156 btn.classList.remove('loading'); 1157 return; 1158 } 1159 1160 // Create subscription 1161 const csrfToken = await fetchCsrfToken(); 1162 const response = await fetch(`${AUTH_CONFIG.API_BASE_URL}${AUTH_CONFIG.SUBSCRIBE_ENDPOINT}`, { 1163 method: 'POST', 1164 headers: { 1165 'Content-Type': 'application/json', 1166 'Accept': 'application/json', 1167 'X-CSRF-TOKEN': csrfToken, 1168 'X-Requested-With': 'XMLHttpRequest' 1169 }, 1170 credentials: 'include', 1171 body: JSON.stringify({ 1172 price_id: priceId, 1173 payment_method: paymentMethod.id 1174 }) 1175 }); 1176 1177 const data = await response.json(); 1178 1179 if (data.requires_action) { 1180 const { error: confirmError } = await modalStripe.confirmCardPayment(data.client_secret); 1181 if (confirmError) { 1182 showModalPaymentError(confirmError.message); 1183 btn.disabled = false; 1184 btn.classList.remove('loading'); 1185 return; 1186 } 1187 1188 // Confirm with backend 1189 await fetch(`${AUTH_CONFIG.API_BASE_URL}${AUTH_CONFIG.SUBSCRIBE_CONFIRM_ENDPOINT}`, { 1190 method: 'POST', 1191 headers: { 1192 'Content-Type': 'application/json', 1193 'Accept': 'application/json', 1194 'X-CSRF-TOKEN': csrfToken 1195 }, 1196 credentials: 'include', 1197 body: JSON.stringify({ subscription_id: data.subscription_id }) 1198 }); 1199 } 1200 1201 if (data.success || response.ok) { 1202 showModalPaymentSuccess(); 1203 } else { 1204 showModalPaymentError(data.message || 'Payment failed. Please try again.'); 1205 btn.disabled = false; 1206 btn.classList.remove('loading'); 1207 } 1208 } catch (error) { 1209 console.error('Modal payment error:', error); 1210 showModalPaymentError('An error occurred. Please try again.'); 1211 btn.disabled = false; 1212 btn.classList.remove('loading'); 1213 } 1214} 1215 1216function showModalPaymentError(message) { 1217 const errorEl = document.getElementById('modal-card-errors'); 1218 if (errorEl) { 1219 errorEl.textContent = message; 1220 errorEl.style.color = '#ff6b6b'; 1221 } 1222} 1223 1224function showModalPaymentSuccess() { 1225 const step2Active = document.getElementById('registerStep2Active'); 1226 const step2Success = document.getElementById('registerStep2Success'); 1227 1228 if (step2Active) step2Active.style.display = 'none'; 1229 if (step2Success) step2Success.style.display = 'flex'; 1230} 1231 1232function skipModalPayment() { 1233 // User chose to skip - redirect to app 1234 window.location.href = registeredUserData?.redirect_url || AUTH_CONFIG.REDIRECT_URL; 1235} 1236 1237// Expose modal payment functions to global scope 1238window.handleModalPaymentSubmit = handleModalPaymentSubmit; 1239window.skipModalPayment = skipModalPayment; 1240 1241// Menu functions 1242function openMenu() { 1243 document.getElementById('menuOverlay').classList.add('active'); 1244} 1245 1246function closeMenu() { 1247 document.getElementById('menuOverlay').classList.remove('active'); 1248} 1249 1250// Expose to global scope for onclick handlers 1251window.openMenu = openMenu; 1252window.closeMenu = closeMenu; 1253window.openAuthModal = openAuthModal; 1254 1255// Close menu when clicking on menu items 1256document.querySelectorAll('.menu-items a').forEach(link => { 1257 link.addEventListener('click', closeMenu); 1258}); 1259 1260// Initialize CSRF token on page load 1261document.addEventListener('DOMContentLoaded', function() { 1262 fetchCsrfToken(); 1263});
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.