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