1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5
5<script> 6 // Keeps a password out of the address bar. Runs first, before anything 7 // else on the page, because the sign-in code only takes over the form once 8 // the sign-in library has loaded, which can take a few seconds. Until 29 Sep 9 // 2026 pressing the button in that gap made the browser send the form the 10 // old way, which writes the email and password into the page address, and 11 // from there into history and analytics. It happened to seven people. 12 (function () { 13 // Anyone who arrives with a password already in the address (an old 14 // bookmark, a shared link) has it removed before analytics can read it. 15 try { 16 var params = new URLSearchParams(window.location.search); 17 var secret = ['password', 'verification-code']; 18 var found = secret.filter(function (name) { return params.has(name); }); 19 if (found.length) { 20 found.forEach(function (name) { params.delete(name); }); 21 var query = params.toString(); 22 history.replaceState(null, '', window.location.pathname + (query ? '?' + query : '') + window.location.hash); 23 } 24 } catch (error) { /* an old browser keeps the address as it is */ } 25 26 // No form on this page is ever sent by the browser itself. A press that 27 // arrives before the sign-in code is ready is held, and run the moment 28 // it is ready, so the person only sees a short wait. 29 var ready = false; 30 var waiting = null; 31 document.addEventListener('submit', function (event) { 32 event.preventDefault(); 33 if (ready) return; 34 waiting = event.target; 35 var button = waiting.querySelector('[type="submit"]'); 36 if (button) { button.disabled = true; button.textContent = 'One moment...'; } 37 }, true); 38 39 // The page calls this once its own form handlers are attached. 40 window.METLY_FORMS_READY = function () { 41 if (ready) return; 42 ready = true; 43 var form = waiting; 44 waiting = null; 45 if (!form) return; 46 var button = form.querySelector('[type="submit"]'); 47 if (button) button.disabled = false; 48 if (typeof form.requestSubmit === 'function') form.requestSubmit(); 49 else form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); 50 }; 51 }()); 52 </script>
52 53 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 54 <!-- App page on app.viewrankai.com: keep it out of search. --> 55 <meta name="robots" content="noindex, follow"> 56 <title>Login - ViewRank AI</title> 57 <link rel="icon" href="/favicon.svg" type="image/svg+xml"> 58 <link rel="icon" href="/favicon.ico" sizes="32x32"> 59 <link rel="apple-touch-icon" href="/apple-touch-icon.png"> 60 <link rel="preconnect" href="https://fonts.googleapis.com"> 61 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 62 <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&display=swap" rel="stylesheet"> 63 <style> 64 :root { 65 --primary: #E87B35; 66 --primary-hover: #D66B28; 67 --bg: #FAFAFA; 68 --surface: #FFFFFF; 69 --surface-muted: #F5F5F5; 70 --border: #E8E8E8; 71 --text: #1A1A1A; 72 --text-secondary: #6B6B6B; 73 --text-muted: #9A9A9A; 74 --radius: 12px; 75 --radius-sm: 8px; 76 --shadow-lg: 0 20px 40px rgba(15, 23, 42, 0.12); 77 } 78 * { margin: 0; padding: 0; box-sizing: border-box; } 79 body { 80 min-height: 100vh; 81 display: flex; 82 align-items: center; 83 justify-content: center; 84 background: radial-gradient(circle at top, #FFF4ED 0, #FAFAFA 45%, #F3F4F6 100%); 85 font-family: 'DM Sans', -apple-system, system-ui, sans-serif; 86 color: var(--text); 87 } 88 .auth-card { 89 background: var(--surface); 90 border-radius: var(--radius); 91 box-shadow: var(--shadow-lg); 92 padding: 32px 32px 28px; 93 width: 100%; 94 max-width: 420px; 95 border: 1px solid var(--border); 96 } 97 .logo-row { 98 display: flex; 99 align-items: center; 100 gap: 10px; 101 justify-content: center; 102 margin-bottom: 16px; 103 } 104 .logo-icon { 105 width: 32px; 106 height: 32px; 107 border-radius: 8px; 108 flex-shrink: 0; 109 display: block; 110 object-fit: cover; 111 } 112 .logo-text { 113 font-weight: 700; 114 font-size: 18px; 115 color: var(--text); 116 } 117 .logo-badge { 118 font-size: 10px; 119 font-weight: 500; 120 padding: 2px 6px; 121 border-radius: 999px; 122 background: var(--surface-muted); 123 color: var(--text-muted); 124 } 125 .auth-title { 126 text-align: center; 127 font-size: 20px; 128 font-weight: 600; 129 margin-bottom: 4px; 130 } 131 .auth-subtitle { 132 text-align: center; 133 font-size: 13px; 134 color: var(--text-secondary); 135 margin-bottom: 20px; 136 } 137 .alert { 138 display: none; 139 padding: 10px 12px; 140 border-radius: var(--radius-sm); 141 font-size: 12px; 142 margin-bottom: 14px; 143 } 144 .alert-error { 145 background: #FEF2F2; 146 border: 1px solid #FCA5A5; 147 color: #B91C1C; 148 } 149 .alert-success { 150 background: #F0FDF4; 151 border: 1px solid #86EFAC; 152 color: #166534; 153 } 154 .form-group { 155 margin-bottom: 16px; 156 } 157 .form-label { 158 display: block; 159 font-size: 13px; 160 font-weight: 500; 161 color: var(--text-secondary); 162 margin-bottom: 6px; 163 } 164 .form-input { 165 width: 100%; 166 padding: 10px 12px; 167 border-radius: var(--radius-sm); 168 border: 1px solid var(--border); 169 font-size: 14px; 170 font-family: inherit; 171 color: var(--text); 172 background: #F9FAFB; 173 transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; 174 } 175 .form-input::placeholder { 176 color: var(--text-muted); 177 } 178 .form-input:focus { 179 outline: none; 180 border-color: var(--primary); 181 background: #fff; 182 box-shadow: 0 0 0 3px rgba(232, 123, 53, 0.15); 183 } 184 .auth-button { 185 width: 100%; 186 margin-top: 4px; 187 padding: 10px 14px; 188 border-radius: var(--radius-sm); 189 border: none; 190 background: var(--primary); 191 color: #fff; 192 font-weight: 600; 193 font-size: 14px; 194 cursor: pointer; 195 transition: background 0.15s ease, transform 0.05s ease, box-shadow 0.15s ease; 196 box-shadow: 0 8px 20px rgba(232, 123, 53, 0.25); 197 } 198 .auth-button:hover { 199 background: var(--primary-hover); 200 transform: translateY(-1px); 201 } 202 .auth-button:disabled { 203 opacity: 0.6; 204 cursor: default; 205 box-shadow: none; 206 } 207 .auth-button.secondary { 208 margin-top: 10px; 209 background: #fff; 210 color: var(--primary); 211 border: 1px solid rgba(232, 123, 53, 0.3); 212 box-shadow: none; 213 } 214 .auth-button.secondary:hover { 215 background: #FFF4ED; 216 } 217 .form-hint { 218 margin-top: 6px; 219 font-size: 12px; 220 color: var(--text-muted); 221 line-height: 1.4; 222 } 223 /* Google sign in. Hidden until Clerk confirms the connection is live, so 224 the page never carries a button that can only fail. */ 225 .social-auth { display: none; } 226 .social-button { 227 width: 100%; 228 display: flex; 229 align-items: center; 230 justify-content: center; 231 gap: 10px; 232 padding: 10px 14px; 233 border-radius: var(--radius-sm); 234 border: 1px solid var(--border); 235 background: #fff; 236 color: var(--text); 237 font-family: inherit; 238 font-weight: 600; 239 font-size: 14px; 240 cursor: pointer; 241 transition: background 0.15s ease, border-color 0.15s ease; 242 } 243 .social-button:hover { 244 background: var(--surface-muted); 245 border-color: #D8D8D8; 246 } 247 .social-button:disabled { opacity: 0.6; cursor: default; } 248 .social-button svg { width: 18px; height: 18px; flex-shrink: 0; } 249 .auth-divider { 250 display: flex; 251 align-items: center; 252 gap: 12px; 253 margin: 16px 0; 254 font-size: 12px; 255 color: var(--text-muted); 256 } 257 .auth-divider::before, 258 .auth-divider::after { 259 content: ''; 260 flex: 1; 261 height: 1px; 262 background: var(--border); 263 } 264 .auth-footer { 265 margin-top: 18px; 266 text-align: center; 267 font-size: 13px; 268 color: var(--text-secondary); 269 } 270 .auth-footer a { 271 color: var(--primary); 272 font-weight: 500; 273 text-decoration: none; 274 } 275 .auth-footer a:hover { 276 text-decoration: underline; 277 } 278 .form-link-row { 279 display: flex; 280 justify-content: flex-end; 281 margin-top: -6px; 282 margin-bottom: 14px; 283 } 284 .text-button { 285 border: none; 286 background: none; 287 color: var(--primary); 288 font: inherit; 289 font-size: 13px; 290 font-weight: 500; 291 cursor: pointer; 292 padding: 0; 293 } 294 .text-button:hover { 295 text-decoration: underline; 296 } 297 .reset-panel { 298 display: none; 299 margin-top: 18px; 300 padding-top: 18px; 301 border-top: 1px solid var(--border); 302 } 303 .reset-copy { 304 font-size: 12px; 305 line-height: 1.5; 306 color: var(--text-secondary); 307 margin-bottom: 14px; 308 } 309 </style> 310</head> 311<body> 312 <div class="auth-card"> 313 <div class="logo-row"> 314 <img class="logo-icon" src="/ViewRank_logo.png" alt="" width="32" height="32">
315 <span class="logo-text">ViewRank AI</span> 316 </div> 317 <h1 class="auth-title">Welcome back</h1> 318 <p class="auth-subtitle">Sign in to view and analyze your Reels.</p> 319 320 <div id="error-message" class="alert alert-error"> 321 <span id="error-text"></span> 322 </div> 323 <div id="success-message" class="alert alert-success"> 324 <span id="success-text"></span> 325 </div> 326 327 <div class="social-auth" id="social-auth"> 328 <button type="button" id="google-button" class="social-button"> 329 <svg viewBox="0 0 48 48" aria-hidden="true" focusable="false"> 330 <path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/> 331 <path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/> 332 <path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/> 333 <path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/> 334 </svg> 335 <span id="google-button-text">Continue with Google</span> 336 </button> 337 <div class="auth-divider">or</div> 338 </div> 339 340 <form id="login-form" method="post" action="#"> 341 <div class="form-group"> 342 <label for="email" class="form-label">Email</label> 343 <input 344 type="email" 345 id="email" 346 name="email" 347 required 348 class="form-input" 349 placeholder="[email protected]" 350 > 351 </div> 352 353 <div class="form-group"> 354 <label for="password" class="form-label">Password</label> 355 <input 356 type="password" 357 id="password" 358 name="password" 359 required 360 class="form-input" 361 placeholder="â¢â¢â¢â¢â¢â¢â¢â¢" 362 > 363 </div> 364 <div class="form-link-row"> 365 <button type="button" id="show-reset-btn" class="text-button">Forgot password?</button> 366 </div> 367 368 <button type="submit" id="login-button" class="auth-button"> 369 Sign In 370 </button> 371 </form> 372 373 <form id="verify-form" method="post" action="#" style="display:none;"> 374 <div class="form-group"> 375 <label for="verification-code" class="form-label">Verification code</label> 376 <input 377 type="text" 378 id="verification-code" 379 name="verification-code" 380 required 381 inputmode="numeric" 382 autocomplete="one-time-code" 383 class="form-input" 384 placeholder="Enter the code from your email" 385 > 386 <p id="verify-hint" class="form-hint">Check your email and paste the code ViewRank sent you.</p> 387 </div> 388 389 <button type="submit" id="verify-button" class="auth-button"> 390 Verify and Sign In 391 </button> 392 <button type="button" id="resend-code-button" class="auth-button secondary"> 393 Send a new code 394 </button> 395 <button type="button" id="back-to-login-button" class="auth-button secondary"> 396 Back to sign in 397 </button> 398 </form> 399 400 <div id="reset-panel" class="reset-panel"> 401 <p class="reset-copy">Enter your email to receive a ViewRank reset code, then set a new password.</p> 402 <form id="reset-request-form" method="post" action="#"> 403 <div class="form-group"> 404 <label for="reset-email" class="form-label">Email</label> 405 <input type="email" id="reset-email" required class="form-input" placeholder="[email protected]"> 406 </div> 407 <button type="submit" id="reset-request-button" class="auth-button">Send Reset Code</button> 408 </form> 409 <form id="reset-complete-form" method="post" action="#" style="display:none;"> 410 <div class="form-group"> 411 <label for="reset-code" class="form-label">Reset code</label> 412 <input type="text" id="reset-code" required class="form-input" placeholder="Enter emailed code"> 413 </div> 414 <div class="form-group"> 415 <label for="new-password" class="form-label">New password</label> 416 <input type="password" id="new-password" required minlength="8" class="form-input" placeholder="New password"> 417 </div> 418 <button type="submit" id="reset-complete-button" class="auth-button">Reset Password</button> 419 </form> 420 </div> 421 422 <div class="auth-footer"> 423 Donât have an account? 424 <a href="https://viewrankai.com/#pricing">Choose a plan</a> 425 </div> 426 </div> 427 428
428<script src="js/config.js?v=20260609-checkout"></script>
428 429
429<script src="js/analytics.js?v=20260927-errors"></script>
429 430
430<script src="js/auth.js?v=20260918-subscribe"></script>
430 431
431<script> 432 document.addEventListener('DOMContentLoaded', async () => { 433 let session = null; 434 try { 435 session = await window.METLY_AUTH.getSession(); 436 } catch (error) { 437 console.warn('Auth not ready:', error); 438 } 439 if (session) { 440 await window.METLY_AUTH.redirectAfterAuth(); 441 return; 442 } 443 444 const loginForm = document.getElementById('login-form'); 445 const verifyForm = document.getElementById('verify-form'); 446 const loginButton = document.getElementById('login-button'); 447 const verifyButton = document.getElementById('verify-button'); 448 const resendCodeButton = document.getElementById('resend-code-button'); 449 const backToLoginButton = document.getElementById('back-to-login-button'); 450 const verificationCodeInput = document.getElementById('verification-code'); 451 const verifyHint = document.getElementById('verify-hint'); 452 const errorMessage = document.getElementById('error-message'); 453 const errorText = document.getElementById('error-text'); 454 const successMessage = document.getElementById('success-message'); 455 const successText = document.getElementById('success-text'); 456 const socialAuth = document.getElementById('social-auth'); 457 const googleButton = document.getElementById('google-button'); 458 const googleButtonText = document.getElementById('google-button-text'); 459 let googleAvailable = false; 460 461 if (new URLSearchParams(window.location.search).get('expired') === '1') { 462 errorText.textContent = 'Your session expired. Please log in again.'; 463 errorMessage.style.display = 'block'; 464 } 465 const resetPanel = document.getElementById('reset-panel'); 466 const resetRequestForm = document.getElementById('reset-request-form'); 467 const resetCompleteForm = document.getElementById('reset-complete-form'); 468 const resetRequestButton = document.getElementById('reset-request-button'); 469 const resetCompleteButton = document.getElementById('reset-complete-button'); 470 471 function showError(message) { 472 successMessage.style.display = 'none'; 473 errorText.textContent = message; 474 errorMessage.style.display = 'block'; 475 } 476 477 function showSuccess(message) { 478 errorMessage.style.display = 'none'; 479 successText.textContent = message; 480 successMessage.style.display = 'block'; 481 } 482 483 function showVerificationStep(result) { 484 loginForm.style.display = 'none'; 485 // Google is not an option once an email sign in is part way through. 486 socialAuth.style.display = 'none'; 487 verifyForm.style.display = 'block'; 488 errorMessage.style.display = 'none'; 489 490 const email = result.email || document.getElementById('email').value.trim(); 491 const isDeviceCheck = result.kind === 'device'; 492 const channel = result.channel || 'email'; 493 494 if (channel === 'authenticator') { 495 successText.textContent = 'Enter the 6-digit code from your authenticator app.'; 496 verifyHint.textContent = 'Open your authenticator app and enter the current code.'; 497 resendCodeButton.style.display = 'none'; 498 } else if (channel === 'phone') { 499 successText.textContent = isDeviceCheck 500 ? `We sent a verification code to your phone to confirm this device.` 501 : `Enter the verification code sent to your phone.`; 502 verifyHint.textContent = 'Check your phone and paste the code ViewRank sent you.'; 503 resendCodeButton.style.display = 'block'; 504 } else { 505 successText.textContent = isDeviceCheck 506 ? `We sent a verification code to ${email} to confirm this device.` 507 : `We sent a verification code to ${email}.`; 508 verifyHint.textContent = 'Check your email and paste the code ViewRank sent you.'; 509 resendCodeButton.style.display = 'block'; 510 } 511 512 successMessage.style.display = 'block'; 513 verificationCodeInput.value = ''; 514 verificationCodeInput.focus(); 515 } 516 517 function showLoginStep() { 518 verifyForm.style.display = 'none'; 519 loginForm.style.display = 'block'; 520 if (googleAvailable) socialAuth.style.display = 'block'; 521 successMessage.style.display = 'none'; 522 errorMessage.style.display = 'none'; 523 verificationCodeInput.value = ''; 524 loginGuard.reset(); 525 verifyGuard.reset(); 526 } 527 528 // Holding the Enter key repeats a form's submit event every few 529 // milliseconds, and disabling the button does not stop it. Every form 530 // on this page runs one attempt at a time. The auth calls have no 531 // timeout of their own, so a call that never answers releases the form 532 // after STALL_MS and invites a retry instead of leaving a dead button. 533 const STALL_MS = 20000; 534 535 const track = (name, props) => { 536 if (window.METLY_ANALYTICS) window.METLY_ANALYTICS.track(name, props); 537 }; 538 539 const failureReason = (error) => 540 String((error && error.message) || 'unknown').replace(/\s+/g, ' ').trim().slice(0, 100); 541 542 // Ask Clerk whether Google is connected before offering it. 543 window.METLY_AUTH.googleEnabled().then((enabled) => { 544 googleAvailable = enabled; 545 if (enabled && verifyForm.style.display !== 'block') { 546 socialAuth.style.display = 'block'; 547 } 548 }); 549 550 const releaseGoogleButton = () => { 551 googleButton.disabled = false;
552 googleButtonText.textContent = 'Continue with Google'; 553 }; 554 555 googleButton.addEventListener('click', async () => { 556 if (googleButton.disabled) return; 557 errorMessage.style.display = 'none'; 558 googleButton.disabled = true; 559 googleButtonText.textContent = 'Taking you to Google...'; 560 track('google_auth_started', { mode: 'login' }); 561 562 // The browser leaves this page when this works, so the timer below 563 // only ever fires when Clerk has not answered. Without it the button 564 // stays disabled for good and the person has nothing left to press. 565 const startedAt = Date.now(); 566 let stalled = false; 567 const stallTimer = setTimeout(() => { 568 stalled = true; 569 releaseGoogleButton(); 570 showError('Google is taking longer than usual. Please try again, or use your email and password below.'); 571 track('google_auth_stalled', { mode: 'login', waited_ms: STALL_MS }); 572 }, STALL_MS); 573 574 try { 575 await window.METLY_AUTH.continueWithGoogle('login'); 576 clearTimeout(stallTimer); 577 } catch (error) { 578 clearTimeout(stallTimer); 579 track('google_auth_failed', { 580 mode: 'login', 581 reason: failureReason(error), 582 duration_ms: Date.now() - startedAt 583 }); 584 if (!stalled) { 585 showError(error.message || 'Could not continue with Google. Please try again.'); 586 releaseGoogleButton(); 587 } 588 } 589 }); 590 591 // Wires one form (or button) to run one attempt at a time. 592 // step: label carried on the login_* events 593 // run: does the work; return true to keep the button locked 594 // because a redirect is about to happen 595 // fallback: error shown when the call fails without a message 596 // cooldownMs: after a success, keep the button locked this long 597 // (stops a mashed "send a new code" sending several) 598 function guard({ element, event = 'submit', button, busyText, idleText, step, run, fallback, countFirst = false, cooldownMs = 0 }) { 599 let inFlight = false; 600 let attempts = 0; 601 602 const unlock = () => { 603 inFlight = false; 604 button.disabled = false; 605 button.textContent = idleText; 606 }; 607 608 element.addEventListener(event, async (e) => { 609 if (e && e.preventDefault) e.preventDefault(); 610 if (inFlight) return; 611 inFlight = true; 612 button.disabled = true; 613 button.textContent = busyText; 614 errorMessage.style.display = 'none'; 615 616 attempts += 1; 617 if (countFirst && attempts === 1) track('login_submitted', { step }); 618 619 const startedAt = Date.now(); 620 let stalled = false; 621 const stallTimer = setTimeout(() => { 622 stalled = true; 623 unlock(); 624 showError('This is taking longer than usual. Please try again.'); 625 track('login_stalled', { step, attempt: attempts, waited_ms: STALL_MS }); 626 }, STALL_MS); 627 628 let keepLocked = false; 629 let failed = false; 630 try { 631 keepLocked = (await run()) === true; 632 } catch (error) { 633 failed = true; 634 track('login_failed', { 635 step, 636 attempt: attempts, 637 reason: failureReason(error), 638 duration_ms: Date.now() - startedAt 639 }); 640 showError(error.message || fallback); 641 } finally { 642 clearTimeout(stallTimer); 643 // After a stall the form is already open again and a fresh 644 // attempt may be running: leave its state alone. 645 if (stalled || keepLocked) return; 646 if (cooldownMs && !failed) { 647 button.textContent = idleText; 648 setTimeout(unlock, cooldownMs); 649 } else { 650 unlock(); 651 } 652 } 653 }); 654 655 return { reset: unlock }; 656 } 657 658 const loginGuard = guard({ 659 element: loginForm, 660 button: loginButton, 661 busyText: 'Signing in...', 662 idleText: 'Sign In', 663 step: 'password', 664 countFirst: true, 665 fallback: 'Failed to sign in. Please check your credentials.', 666 run: async () => { 667 successMessage.style.display = 'none'; 668 const email = document.getElementById('email').value; 669 const password = document.getElementById('password').value; 670 const result = await window.METLY_AUTH.login(email, password); 671 if (result?.status === 'needs_verification') { 672 showVerificationStep(result); 673 } 674 // Stay locked: either the redirect follows, or the code step is 675 // showing and the back button reopens this form. 676 return true; 677 } 678 }); 679 680 const verifyGuard = guard({ 681 element: verifyForm, 682 button: verifyButton, 683 busyText: 'Verifying...', 684 idleText: 'Verify and Sign In', 685 step: 'code', 686 fallback: 'Failed to verify code.', 687 run: async () => { 688 await window.METLY_AUTH.verifyLoginCode(verificationCodeInput.value.trim()); 689 // Stay locked: the redirect to the workspace follows. 690 return true; 691 } 692 }); 693 694 guard({ 695 element: resendCodeButton, 696 event: 'click', 697 button: resendCodeButton, 698 busyText: 'Sending...', 699 idleText: 'Send a new code', 700 step: 'resend', 701 cooldownMs: 10000, 702 fallback: 'Failed to resend verification code.', 703 run: async () => { 704 await window.METLY_AUTH.resendLoginVerificationCode(); 705 showSuccess('A new verification code has been sent.'); 706 } 707 }); 708 709 backToLoginButton.addEventListener('click', () => { 710 showLoginStep(); 711 }); 712 713 document.getElementById('show-reset-btn')?.addEventListener('click', () => { 714 resetPanel.style.display = resetPanel.style.display === 'block' ? 'none' : 'block'; 715 const email = document.getElementById('email').value.trim(); 716 if (email) document.getElementById('reset-email').value = email; 717 }); 718 719 guard({ 720 element: resetRequestForm, 721 button: resetRequestButton, 722 busyText: 'Sending...', 723 idleText: 'Send Reset Code', 724 step: 'reset_request', 725 fallback: 'Failed to send reset code.', 726 run: async () => { 727 successMessage.style.display = 'none'; 728 await window.METLY_AUTH.startPasswordReset(document.getElementById('reset-email').value.trim()); 729 resetRequestForm.style.display = 'none'; 730 resetCompleteForm.style.display = 'block'; 731 showSuccess('Reset code sent. Check your email, then enter the code and a new password.'); 732 // Stay locked: this form is hidden from here on. 733 return true; 734 } 735 }); 736 737 guard({ 738 element: resetCompleteForm, 739 button: resetCompleteButton,
740 busyText: 'Resetting...', 741 idleText: 'Reset Password', 742 step: 'reset_complete', 743 fallback: 'Failed to reset password.', 744 run: async () => { 745 successMessage.style.display = 'none'; 746 await window.METLY_AUTH.completePasswordReset( 747 document.getElementById('reset-code').value.trim(), 748 document.getElementById('new-password').value 749 ); 750 // Stay locked: the redirect to the workspace follows. 751 return true; 752 } 753 }); 754 755 // Every handler is attached: a press held while the page loaded runs now. 756 if (window.METLY_FORMS_READY) window.METLY_FORMS_READY(); 757 }); 758 </script>
758 759</body> 760</html>
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.