PageSourceSearch

https://xpathlabs.ai/google-signin.js

js xpathlabs.ai collected 2026-10-05 13:20:32 UTC 38,903 bytes, 802 lines download raw bytes

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;">&times;</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.