1/** 2 * Google Sign-In Module 3 * Shared authentication logic for landing pages and sign-in page 4 */ 5 6window.GoogleSignIn = { 7 /** 8 * Show the Get Started modal with sign-up options 9 */ 10 showGetStartedModal() { 11 const self = this; 12 13 // Remove existing modal if any 14 const existing = document.getElementById('getStartedModal'); 15 if (existing) existing.remove(); 16 17 const modal = document.createElement('div'); 18 modal.id = 'getStartedModal'; 19 modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(15,23,42,0.6);display:flex;align-items:center;justify-content:center;z-index:9999;opacity:1;visibility:visible;backdrop-filter:blur(4px);'; 20 21 modal.innerHTML = ` 22 <div style="background:#fff;border-radius:16px;max-width:480px;width:92%;padding:3rem;position:relative;box-shadow:0 32px 80px rgba(0,0,0,0.18);animation:modalSlideIn 0.25s ease-out;"> 23 <style> 24 @keyframes modalSlideIn { 25 from { opacity: 0; transform: translateY(-20px) scale(0.98); } 26 to { opacity: 1; transform: translateY(0) scale(1); } 27 } 28 #getStartedModal input:focus { 29 outline: none; 30 border-color: #3B82F6; 31 box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); 32 } 33 #modalGoogleSignIn:hover { 34 background: #F8FAFC !important; 35 border-color: #CBD5E1 !important; 36 } 37 #modalEmailForm button[type="submit"]:hover, 38 #verifyCodeBtn:hover { 39 transform: translateY(-1px); 40 box-shadow: 0 8px 24px rgba(59, 130, 246, 0.25); 41 } 42 #togglePassword:hover { 43 color: #64748B; 44 } 45 .code-input { 46 width: 48px; 47 height: 56px; 48 text-align: center; 49 font-size: 1.5rem; 50 font-weight: 600; 51 border: 2px solid #E2E8F0; 52 border-radius: 10px; 53 transition: all 0.15s; 54 color: #0F172A; 55 } 56 .code-input:focus { 57 outline: none; 58 border-color: #3B82F6; 59 box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); 60 } 61 #resendCodeBtn:disabled { 62 opacity: 0.5; 63 cursor: not-allowed; 64 } 65 </style> 66 <button id="closeGetStartedModal" style="position:absolute;top:1.25rem;right:1.25rem;background:#F1F5F9;border:none;width:36px;height:36px;border-radius:50%;font-size:1.25rem;cursor:pointer;color:#64748B;line-height:1;display:flex;align-items:center;justify-content:center;transition:all 0.15s;">×</button> 67 68 <!-- Screen 1: Sign Up Form --> 69 <div id="signupScreen"> 70 <div style="text-align:center;margin-bottom:2rem;"> 71 <img src="/images/xpathlabs.jpg" alt="XPathLabs" style="height:40px;margin-bottom:1.5rem;"> 72 <h2 style="margin:0;font-size:1.75rem;font-weight:700;color:#0F172A;letter-spacing:-0.02em;">See the live preview in seconds</h2> 73 <p style="margin:0.75rem 0 0;font-size:0.875rem;color:#64748B;"> 74 Already have an account? <a href="/workspace.html" style="color:#3B82F6;text-decoration:none;font-weight:600;">Sign in</a> 75 </p> 76 </div> 77 78 <button id="modalGoogleSignIn" style="width:100%;padding:1rem 1.25rem;border:2px solid #E2E8F0;border-radius:10px;background:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:0.75rem;font-size:1rem;font-weight:600;color:#0F172A;transition:all 0.15s;">
79 <img src="https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/google.svg" alt="Google" style="width:22px;height:22px;"> 80 Continue with Google 81 </button> 82 83 <div style="display:flex;align-items:center;margin:1.5rem 0;"> 84 <div style="flex:1;height:1px;background:#E2E8F0;"></div> 85 <span style="padding:0 1rem;color:#94A3B8;font-size:0.875rem;font-weight:500;">or</span> 86 <div style="flex:1;height:1px;background:#E2E8F0;"></div> 87 </div> 88 89 <form id="modalEmailForm" style="margin:0;"> 90 <div id="modalErrorMessage" style="display:none;background:#FEF2F2;border:1px solid #FECACA;border-radius:8px;padding:0.75rem 1rem;margin-bottom:1rem;color:#DC2626;font-size:0.875rem;line-height:1.4;"> 91 </div> 92 <div style="margin-bottom:1.25rem;"> 93 <label style="display:block;font-size:0.875rem;font-weight:600;color:#334155;margin-bottom:0.5rem;">Email address</label> 94 <input type="email" id="modalEmail" placeholder="[email protected]" required style="width:100%;padding:0.875rem 1rem;border:2px solid #E2E8F0;border-radius:10px;font-size:1rem;box-sizing:border-box;transition:all 0.15s;color:#0F172A;"> 95 </div> 96 <div style="margin-bottom:1.5rem;"> 97 <label style="display:block;font-size:0.875rem;font-weight:600;color:#334155;margin-bottom:0.5rem;">Password</label> 98 <div style="position:relative;"> 99 <input type="password" id="modalPassword" placeholder="Create a strong password" required style="width:100%;padding:0.875rem 2.75rem 0.875rem 1rem;border:2px solid #E2E8F0;border-radius:10px;font-size:1rem;box-sizing:border-box;transition:all 0.15s;color:#0F172A;"> 100 <button type="button" id="togglePassword" style="position:absolute;right:0.75rem;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;padding:0.25rem;color:#94A3B8;display:flex;align-items:center;justify-content:center;" aria-label="Toggle password visibility"> 101 <svg id="eyeIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 102 <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path> 103 <circle cx="12" cy="12" r="3"></circle> 104 </svg> 105 </button> 106 </div> 107 <div id="passwordRequirements" style="margin-top:0.625rem;font-size:0.8125rem;display:grid;grid-template-columns:1fr 1fr;gap:0.25rem 1rem;"> 108 <div id="req-length" style="display:flex;align-items:center;gap:0.375rem;color:#94A3B8;"> 109 <span class="req-icon" style="width:14px;height:14px;display:flex;align-items:center;justify-content:center;">â</span> 110 <span>8+ characters</span> 111 </div> 112 <div id="req-uppercase" style="display:flex;align-items:center;gap:0.375rem;color:#94A3B8;"> 113 <span class="req-icon" style="width:14px;height:14px;display:flex;align-items:center;justify-content:center;">â</span> 114 <span>Uppercase letter</span> 115 </div> 116 <div id="req-lowercase" style="display:flex;align-items:center;gap:0.375rem;color:#94A3B8;"> 117 <span class="req-icon" style="width:14px;height:14px;display:flex;align-items:center;justify-content:center;">â</span> 118 <span>Lowercase letter</span> 119 </div> 120 <div id="req-number" style="display:flex;align-items:center;gap:0.375rem;color:#94A3B8;">
121 <span class="req-icon" style="width:14px;height:14px;display:flex;align-items:center;justify-content:center;">â</span> 122 <span>Number</span> 123 </div> 124 </div> 125 </div> 126 <button type="submit" style="width:100%;padding:1rem 1.25rem;border:none;border-radius:10px;background:linear-gradient(135deg, #3B82F6 0%, #2563EB 100%);color:#fff;cursor:pointer;font-size:1rem;font-weight:600;transition:all 0.15s;box-shadow:0 4px 12px rgba(59, 130, 246, 0.25);"> 127 Create Account 128 </button> 129 </form> 130 131 <p style="text-align:center;margin:1.5rem 0 0;font-size:0.8125rem;color:#94A3B8;line-height:1.5;"> 132 By signing up, you agree to our <a href="/terms-of-service.html" style="color:#3B82F6;text-decoration:none;font-weight:500;">Terms of Service</a> and <a href="/privacy-policy.html" style="color:#3B82F6;text-decoration:none;font-weight:500;">Privacy Policy</a> 133 </p> 134 </div> 135 136 <!-- Screen 2: Email Verification --> 137 <div id="verificationScreen" style="display:none;"> 138 <div style="text-align:center;margin-bottom:2rem;"> 139 <div style="width:64px;height:64px;background:#EFF6FF;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 1.5rem;"> 140 <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#3B82F6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 141 <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path> 142 <polyline points="22,6 12,13 2,6"></polyline> 143 </svg> 144 </div> 145 <h2 style="margin:0 0 0.5rem;font-size:1.75rem;font-weight:700;color:#0F172A;letter-spacing:-0.02em;">Check your email</h2> 146 <p style="margin:0;font-size:1rem;color:#64748B;">We sent a verification code to</p> 147 <p id="verificationEmail" style="margin:0.25rem 0 0;font-size:1rem;font-weight:600;color:#0F172A;"></p> 148 </div> 149 150 <div id="verificationErrorMessage" style="display:none;background:#FEF2F2;border:1px solid #FECACA;border-radius:8px;padding:0.75rem 1rem;margin-bottom:1rem;color:#DC2626;font-size:0.875rem;line-height:1.4;"> 151 </div> 152 153 <div style="margin-bottom:1.5rem;"> 154 <label style="display:block;font-size:0.875rem;font-weight:600;color:#334155;margin-bottom:0.75rem;text-align:center;">Enter 8-digit code</label> 155 <div style="display:flex;justify-content:center;gap:0.375rem;"> 156 <input type="text" maxlength="1" class="code-input" data-index="0" inputmode="numeric" pattern="[0-9]"> 157 <input type="text" maxlength="1" class="code-input" data-index="1" inputmode="numeric" pattern="[0-9]"> 158 <input type="text" maxlength="1" class="code-input" data-index="2" inputmode="numeric" pattern="[0-9]"> 159 <input type="text" maxlength="1" class="code-input" data-index="3" inputmode="numeric" pattern="[0-9]"> 160 <input type="text" maxlength="1" class="code-input" data-index="4" inputmode="numeric" pattern="[0-9]"> 161 <input type="text" maxlength="1" class="code-input" data-index="5" inputmode="numeric" pattern="[0-9]"> 162 <input type="text" maxlength="1" class="code-input" data-index="6" inputmode="numeric" pattern="[0-9]"> 163 <input type="text" maxlength="1" class="code-input" data-index="7" inputmode="numeric" pattern="[0-9]"> 164 </div> 165 </div> 166 167 <button id="verifyCodeBtn" style="width:100%;padding:1rem 1.25rem;border:none;border-radius:10px;background:linear-gradient(135deg, #3B82F6 0%, #2563EB 100%);color:#fff;cursor:pointer;font-size:1rem;font-weight:600;transition:all 0.15s;box-shadow:0 4px 12px rgba(59, 130, 246, 0.25);margin-bottom:1rem;"> 168 Verify Email 169 </button> 170 171 <div style="text-align:center;"> 172 <p style="margin:0 0 0.5rem;font-size:0.875rem;color:#64748B;"> 173 Didn't receive the code? 174 <button id="resendCodeBtn" style="background:none;border:none;color:#3B82F6;font-weight:600;cursor:pointer;font-size:0.875rem;" disabled> 175 Resend <span id="countdownTimer">(60s)</span> 176 </button> 177 </p> 178 <button id="changeEmailBtn" style="background:none;border:none;color:#64748B;font-size:0.875rem;cursor:pointer;text-decoration:underline;"> 179 Change email address 180 </button> 181 </div> 182 </div> 183 </div> 184 `; 185 186 document.body.appendChild(modal); 187 188 // Cleanup function to clear intervals before removing modal 189 const closeModal = () => { 190 if (countdownInterval) clearInterval(countdownInterval); 191 modal.remove(); 192 }; 193 194 // Close modal on backdrop click 195 modal.addEventListener('click', (e) => { 196 if (e.target === modal) closeModal(); 197 }); 198 199 // Close button 200 document.getElementById('closeGetStartedModal').addEventListener('click', closeModal); 201 202 // Google sign-in button 203 document.getElementById('modalGoogleSignIn').addEventListener('click', () => { 204 closeModal(); 205 self.signIn(); 206 }); 207 208 // Screen elements 209 const signupScreen = document.getElementById('signupScreen'); 210 const verificationScreen = document.getElementById('verificationScreen'); 211 212 // Store credentials for later use after verification 213 let pendingEmail = ''; 214 let pendingPassword = ''; 215 let countdownInterval = null; 216 217 // Error handling for signup screen 218 const errorMessageEl = document.getElementById('modalErrorMessage'); 219 const showError = (message) => { 220 errorMessageEl.textContent = message; 221 errorMessageEl.style.display = 'block'; 222 }; 223 const hideError = () => { 224 errorMessageEl.style.display = 'none'; 225 // Reset to default error styles 226 errorMessageEl.style.background = '#FEF2F2'; 227 errorMessageEl.style.borderColor = '#FECACA'; 228 errorMessageEl.style.color = '#DC2626'; 229 }; 230 231 // Error handling for verification screen 232 const verificationErrorEl = document.getElementById('verificationErrorMessage'); 233 const showVerificationError = (message) => { 234 verificationErrorEl.textContent = message; 235 verificationErrorEl.style.display = 'block'; 236 }; 237 const hideVerificationError = () => { 238 verificationErrorEl.style.display = 'none'; 239 }; 240 241 // Start countdown timer 242 const startCountdown = () => { 243 let seconds = 60; 244 const timerEl = document.getElementById('countdownTimer'); 245 const resendBtn = document.getElementById('resendCodeBtn'); 246 247 resendBtn.disabled = true; 248 timerEl.textContent = `(${seconds}s)`; 249 timerEl.style.display = 'inline'; 250 251 countdownInterval = setInterval(() => { 252 seconds--; 253 if (seconds <= 0) { 254 clearInterval(countdownInterval); 255 resendBtn.disabled = false;
256 timerEl.style.display = 'none'; 257 } else { 258 timerEl.textContent = `(${seconds}s)`; 259 } 260 }, 1000); 261 }; 262 263 // Switch to verification screen 264 const showVerificationScreen = (email) => { 265 signupScreen.style.display = 'none'; 266 verificationScreen.style.display = 'block'; 267 document.getElementById('verificationEmail').textContent = email; 268 269 // Clear code inputs 270 const codeInputs = document.querySelectorAll('.code-input'); 271 codeInputs.forEach(input => input.value = ''); 272 codeInputs[0].focus(); 273 274 hideVerificationError(); 275 startCountdown(); 276 }; 277 278 // Switch back to signup screen 279 const showSignupScreen = () => { 280 if (countdownInterval) clearInterval(countdownInterval); 281 verificationScreen.style.display = 'none'; 282 signupScreen.style.display = 'block'; 283 }; 284 285 // Send verification code 286 const sendVerificationCode = async (email) => { 287 const response = await fetch('/api/auth/send-verification-code', { 288 method: 'POST', 289 headers: { 'Content-Type': 'application/json' }, 290 body: JSON.stringify({ email }) 291 }); 292 293 // Check if response is JSON 294 const contentType = response.headers.get('content-type'); 295 if (!contentType || !contentType.includes('application/json')) { 296 throw new Error('Server error. Please try again.'); 297 } 298 299 const data = await response.json(); 300 301 if (!response.ok) { 302 const error = new Error(data.error || 'Failed to send verification code'); 303 error.code = data.code; 304 throw error; 305 } 306 307 return data; 308 }; 309 310 // Email form submission - send verification code 311 document.getElementById('modalEmailForm').addEventListener('submit', async (e) => { 312 e.preventDefault(); 313 hideError(); 314 315 const email = document.getElementById('modalEmail').value.trim(); 316 const password = document.getElementById('modalPassword').value; 317 const submitBtn = e.target.querySelector('button[type="submit"]'); 318 319 if (!email || !password) { 320 showError('Please enter email and password.'); 321 return; 322 } 323 324 if (!validatePassword(password)) { 325 showError('Password must be at least 8 characters with uppercase, lowercase, and a number.'); 326 return; 327 } 328 329 submitBtn.disabled = true; 330 submitBtn.textContent = 'Sending code...'; 331 332 try { 333 await sendVerificationCode(email); 334 pendingEmail = email; 335 pendingPassword = password; 336 showVerificationScreen(email); 337 } catch (error) { 338 if (error.code === 'USER_EXISTS') { 339 // Show warning message for existing users with sign-in link 340 errorMessageEl.innerHTML = 'You already have an account with us. <a href="/workspace.html" style="color:#B45309;font-weight:600;text-decoration:underline;">Sign in instead</a>'; 341 errorMessageEl.style.display = 'block'; 342 errorMessageEl.style.background = '#FFFBEB'; 343 errorMessageEl.style.borderColor = '#FCD34D'; 344 errorMessageEl.style.color = '#B45309'; 345 } else { 346 showError(error.message || 'Failed to send verification code. Please try again.'); 347 } 348 } finally { 349 submitBtn.disabled = false; 350 submitBtn.textContent = 'Create Account'; 351 } 352 }); 353 354 // Code input handling 355 const codeInputs = document.querySelectorAll('.code-input'); 356 357 codeInputs.forEach((input, index) => { 358 // Auto-focus next input on entry 359 input.addEventListener('input', (e) => { 360 const value = e.target.value; 361 // Only allow digits 362 e.target.value = value.replace(/[^0-9]/g, ''); 363 364 if (e.target.value && index < codeInputs.length - 1) { 365 codeInputs[index + 1].focus(); 366 } 367 }); 368 369 // Handle backspace to go to previous input 370 input.addEventListener('keydown', (e) => { 371 if (e.key === 'Backspace' && !e.target.value && index > 0) { 372 codeInputs[index - 1].focus(); 373 } 374 }); 375 376 // Handle paste - distribute digits across inputs 377 input.addEventListener('paste', (e) => { 378 e.preventDefault(); 379 const pastedData = e.clipboardData.getData('text').replace(/[^0-9]/g, '').slice(0, 8); 380 pastedData.split('').forEach((char, i) => { 381 if (codeInputs[i]) { 382 codeInputs[i].value = char; 383 } 384 });
385 // Focus last filled input or the next empty one 386 const focusIndex = Math.min(pastedData.length, codeInputs.length - 1); 387 codeInputs[focusIndex].focus(); 388 }); 389 }); 390 391 // Get full code from inputs 392 const getCode = () => { 393 return Array.from(codeInputs).map(input => input.value).join(''); 394 }; 395 396 // Verify code button 397 document.getElementById('verifyCodeBtn').addEventListener('click', async () => { 398 hideVerificationError(); 399 const code = getCode(); 400 401 if (code.length !== 8) { 402 showVerificationError('Please enter the complete 8-digit code.'); 403 return; 404 } 405 406 const verifyBtn = document.getElementById('verifyCodeBtn'); 407 verifyBtn.disabled = true; 408 verifyBtn.textContent = 'Verifying...'; 409 410 try { 411 // Verify the code 412 const verifyResponse = await fetch('/api/auth/verify-code', { 413 method: 'POST', 414 headers: { 'Content-Type': 'application/json' }, 415 body: JSON.stringify({ email: pendingEmail, code }) 416 }); 417 418 if (!verifyResponse.ok) { 419 const error = await verifyResponse.json(); 420 throw new Error(error.error || 'Invalid verification code'); 421 } 422 423 // Code verified - now create the account 424 verifyBtn.textContent = 'Creating account...'; 425 await self.signInWithEmail(pendingEmail, pendingPassword, showVerificationError); 426 closeModal(); 427 } catch (error) { 428 showVerificationError(error.message || 'Verification failed. Please try again.'); 429 verifyBtn.disabled = false; 430 verifyBtn.textContent = 'Verify Email'; 431 } 432 }); 433 434 // Resend code button 435 document.getElementById('resendCodeBtn').addEventListener('click', async () => { 436 const resendBtn = document.getElementById('resendCodeBtn'); 437 resendBtn.disabled = true; 438 439 try { 440 await sendVerificationCode(pendingEmail); 441 // Clear code inputs 442 codeInputs.forEach(input => input.value = ''); 443 codeInputs[0].focus(); 444 hideVerificationError(); 445 startCountdown(); 446 } catch (error) { 447 showVerificationError('Failed to resend code. Please try again.'); 448 resendBtn.disabled = false; 449 } 450 }); 451 452 // Change email button 453 document.getElementById('changeEmailBtn').addEventListener('click', () => { 454 showSignupScreen(); 455 }); 456 457 // Hover effects 458 const googleBtn = document.getElementById('modalGoogleSignIn'); 459 googleBtn.addEventListener('mouseenter', () => { 460 googleBtn.style.background = '#F8FAFC'; 461 googleBtn.style.borderColor = '#CBD5E1'; 462 }); 463 googleBtn.addEventListener('mouseleave', () => { 464 googleBtn.style.background = '#fff'; 465 googleBtn.style.borderColor = '#E2E8F0'; 466 }); 467 468 const closeBtn = document.getElementById('closeGetStartedModal'); 469 closeBtn.addEventListener('mouseenter', () => { 470 closeBtn.style.background = '#E2E8F0'; 471 closeBtn.style.color = '#334155'; 472 }); 473 closeBtn.addEventListener('mouseleave', () => { 474 closeBtn.style.background = '#F1F5F9'; 475 closeBtn.style.color = '#64748B'; 476 }); 477 478 // Hide error when user starts typing 479 document.getElementById('modalEmail').addEventListener('input', hideError); 480 document.getElementById('modalPassword').addEventListener('input', hideError); 481 482 // Password strength validation 483 const updateRequirement = (id, met) => { 484 const el = document.getElementById(id); 485 if (!el) return; 486 const icon = el.querySelector('.req-icon'); 487 if (met) { 488 el.style.color = '#059669'; 489 icon.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>'; 490 } else { 491 el.style.color = '#94A3B8'; 492 icon.textContent = 'â'; 493 } 494 }; 495 496 const validatePassword = (password) => { 497 const checks = { 498 length: password.length >= 8, 499 uppercase: /[A-Z]/.test(password), 500 lowercase: /[a-z]/.test(password), 501 number: /[0-9]/.test(password) 502 };
503 updateRequirement('req-length', checks.length); 504 updateRequirement('req-uppercase', checks.uppercase); 505 updateRequirement('req-lowercase', checks.lowercase); 506 updateRequirement('req-number', checks.number); 507 return checks.length && checks.uppercase && checks.lowercase && checks.number; 508 }; 509 510 document.getElementById('modalPassword').addEventListener('input', (e) => { 511 validatePassword(e.target.value); 512 }); 513 514 // Password visibility toggle 515 const toggleBtn = document.getElementById('togglePassword'); 516 const passwordInput = document.getElementById('modalPassword'); 517 const eyeIcon = document.getElementById('eyeIcon'); 518 519 toggleBtn.addEventListener('click', () => { 520 const isPassword = passwordInput.type === 'password'; 521 passwordInput.type = isPassword ? 'text' : 'password'; 522 523 // Update icon - show eye-off when password is visible 524 if (isPassword) { 525 eyeIcon.innerHTML = ` 526 <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path> 527 <line x1="1" y1="1" x2="23" y2="23"></line> 528 `; 529 } else { 530 eyeIcon.innerHTML = ` 531 <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path> 532 <circle cx="12" cy="12" r="3"></circle> 533 `; 534 } 535 }); 536 }, 537 538 /** 539 * Initialize and perform Google Sign-In 540 * @param {Object} options - Configuration options 541 * @param {string} options.redirectUrl - URL to redirect after successful sign-in (default: /meta-performance-manager.html) 542 * @param {boolean} options.isNewUserRedirect - Add ?new_user=true for new users (default: true) 543 */ 544 async signIn(options = {}) { 545 const { 546 redirectUrl = '/meta-performance-manager.html', 547 isNewUserRedirect = true 548 } = options; 549 550 try { 551 // Initialize Firebase if not already done 552 const fb = await window.FirebaseAuth?.initialize(); 553 if (!fb) { 554 alert('Authentication service unavailable. Please try again.'); 555 return; 556 } 557 558 // Create Google provider 559 const provider = new firebase.auth.GoogleAuthProvider(); 560 provider.addScope('email'); 561 provider.addScope('profile'); 562 563 // Sign in with popup 564 const result = await firebase.auth().signInWithPopup(provider); 565 const user = result.user; 566 567 if (!user) { 568 throw new Error('No user returned from Google sign-in'); 569 } 570 571 // Get Firebase token 572 const token = await user.getIdToken(); 573 574 // Store token 575 localStorage.setItem('authToken', token); 576 localStorage.setItem('authTokenExpiry', (Date.now() + 3600000).toString()); 577 578 // Check/create user in backend 579 const response = await fetch('/api/auth/google-signin', { 580 method: 'POST', 581 headers: { 582 'Authorization': `Bearer ${token}`, 583 'Content-Type': 'application/json' 584 }, 585 body: JSON.stringify({ 586 email: user.email, 587 name: user.displayName, 588 photoURL: user.photoURL 589 }) 590 }); 591 592 if (!response.ok) { 593 const error = await response.json(); 594 throw new Error(error.error || 'Failed to authenticate'); 595 } 596 597 const data = await response.json(); 598 599 // Clear old session data to prevent stale values affecting role/subscription checks 600 sessionStorage.removeItem('userRole'); 601 sessionStorage.removeItem('subscription'); 602 sessionStorage.removeItem('hasFullAccess'); 603 sessionStorage.removeItem('permissions'); 604 sessionStorage.removeItem('selectedBrand'); 605 606 // Store session data 607 sessionStorage.setItem('isAuthenticated', 'true'); 608 sessionStorage.setItem('userEmail', user.email.toLowerCase()); 609 sessionStorage.setItem('userRole', data.user?.role || 'trial_user'); 610 611 if (data.user?.brandId) { 612 sessionStorage.setItem('selectedBrand', data.user.brandId); 613 } 614 if (data.user?.permissions) { 615 sessionStorage.setItem('permissions', JSON.stringify(data.user.permissions)); 616 } 617 if (data.user?.hasFullAccess !== undefined) { 618 sessionStorage.setItem('hasFullAccess', data.user.hasFullAccess ? 'true' : 'false'); 619 } 620 if (data.user?.subscription) { 621 sessionStorage.setItem('subscription', JSON.stringify(data.user.subscription)); 622 } 623 624 // Redirect to app 625 if (isNewUserRedirect && data.isNew) { 626 window.location.href = `${redirectUrl}?new_user=true`; 627 } else { 628 window.location.href = redirectUrl; 629 } 630 631 } catch (error) { 632 // Handle specific errors 633 if (error.code === 'auth/popup-closed-by-user') { 634 // User closed popup - don't show error 635 return; 636 } 637 if (error.code === 'auth/popup-blocked') { 638 alert('Please allow popups for this site to sign in with Google.'); 639 return; 640 } 641 642 alert('Sign-in failed: ' + (error.message || 'Please try again.')); 643 } 644 }, 645 646 /** 647 * Sign in or sign up with email and password 648 * @param {string} email - User email 649 * @param {string} password - User password 650 * @param {function} showError - Callback to display inline error message 651 */ 652 async signInWithEmail(email, password, showError) { 653 const redirectUrl = '/meta-performance-manager.html'; 654 const displayError = showError || ((msg) => alert(msg)); 655 656 try { 657 // Initialize Firebase if not already done 658 const fb = await window.FirebaseAuth?.initialize(); 659 if (!fb) { 660 displayError('Authentication service unavailable. Please try again.'); 661 throw new Error('Firebase not initialized'); 662 } 663 664 let user; 665 let isNewUser = false;
666 667 // Try to create a new user first 668 try { 669 const userCredential = await firebase.auth().createUserWithEmailAndPassword(email, password); 670 user = userCredential.user; 671 isNewUser = true; 672 } catch (createError) { 673 // If email already exists, try to sign in instead 674 if (createError.code === 'auth/email-already-in-use') { 675 try { 676 const userCredential = await firebase.auth().signInWithEmailAndPassword(email, password); 677 user = userCredential.user; 678 } catch (signInError) { 679 // Wrong password or other error 680 if (signInError.code === 'auth/wrong-password' || signInError.code === 'auth/invalid-credential') { 681 displayError('Incorrect password. Please try again.'); 682 } else if (signInError.code === 'auth/too-many-requests') { 683 displayError('Too many failed attempts. Please try again later.'); 684 } else { 685 displayError('Sign-in failed. Please try again.'); 686 } 687 throw signInError; 688 } 689 } else if (createError.code === 'auth/weak-password') { 690 displayError('Password is too weak. Please use at least 8 characters.'); 691 throw createError; 692 } else if (createError.code === 'auth/invalid-email') { 693 displayError('Please enter a valid email address.'); 694 throw createError; 695 } else { 696 displayError('Sign-up failed. Please try again.'); 697 throw createError; 698 } 699 } 700 701 if (!user) { 702 throw new Error('No user returned from authentication'); 703 } 704 705 // Get Firebase token 706 const token = await user.getIdToken(); 707 708 // Store token 709 localStorage.setItem('authToken', token); 710 localStorage.setItem('authTokenExpiry', (Date.now() + 3600000).toString()); 711 712 // Create/update user in backend (reuse the google-signin endpoint) 713 const response = await fetch('/api/auth/google-signin', { 714 method: 'POST', 715 headers: { 716 'Authorization': `Bearer ${token}`, 717 'Content-Type': 'application/json' 718 }, 719 body: JSON.stringify({ 720 email: user.email, 721 name: user.displayName || email.split('@')[0], 722 authProvider: 'email' 723 }) 724 }); 725 726 if (!response.ok) { 727 const error = await response.json(); 728 displayError(error.error || 'Failed to authenticate. Please try again.'); 729 throw new Error(error.error || 'Failed to authenticate'); 730 } 731 732 const data = await response.json(); 733 734 // Store session data 735 sessionStorage.setItem('isAuthenticated', 'true'); 736 sessionStorage.setItem('userEmail', user.email.toLowerCase()); 737 sessionStorage.setItem('userRole', data.user?.role || 'trial_user'); 738 739 if (data.user?.brandId) { 740 sessionStorage.setItem('selectedBrand', data.user.brandId); 741 } 742 if (data.user?.permissions) { 743 sessionStorage.setItem('permissions', JSON.stringify(data.user.permissions)); 744 } 745 if (data.user?.hasFullAccess !== undefined) { 746 sessionStorage.setItem('hasFullAccess', data.user.hasFullAccess ? 'true' : 'false'); 747 } 748 if (data.user?.subscription) { 749 sessionStorage.setItem('subscription', JSON.stringify(data.user.subscription)); 750 } 751 752 // Redirect to app 753 if (data.isNew || isNewUser) { 754 window.location.href = `${redirectUrl}?new_user=true`; 755 } else { 756 window.location.href = redirectUrl; 757 } 758 759 } catch (error) { 760 throw error; // Re-throw so the form handler can reset the button 761 } 762 }, 763 764 /** 765 * Attach sign-in to buttons by ID 766 * @param {string[]} buttonIds - Array of button element IDs 767 * @param {Object} options - Configuration options 768 * @param {boolean} options.showModal - Show modal with options (default: true for landing pages) 769 * @param {boolean} options.directSignIn - Skip modal and sign in directly (for sign-in page) 770 */ 771 attachToButtons(buttonIds, options = {}) { 772 const { showModal = true, directSignIn = false } = options; 773 const self = this; 774 775 const attach = () => { 776 buttonIds.forEach(id => { 777 const btn = document.getElementById(id); 778 if (btn) { 779 btn.onclick = (e) => { 780 e.preventDefault(); 781 // Track CTA click in Google Analytics 782 window.trackEvent?.('cta_click', { button_id: id, button_text: btn.textContent?.trim() }); 783 if (directSignIn) { 784 self.signIn(options); 785 } else if (showModal) { 786 self.showGetStartedModal(); 787 } else { 788 self.signIn(options); 789 } 790 }; 791 } 792 }); 793 }; 794 795 // If DOM is already loaded, attach immediately; otherwise wait 796 if (document.readyState === 'loading') { 797 document.addEventListener('DOMContentLoaded', attach); 798 } else { 799 attach(); 800 } 801 } 802};
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.