PageSourceSearch

https://assets.globalsku.app/fddf5947-62df-44e7-b2e0-59bb7461689d/js/landing-inline.js

js globalsku.app collected 2026-09-24 14:07:53 UTC 39,231 bytes, 1,263 lines download raw bytes

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.