1// Multi-Factor Authentication Modal Module 2 3var MultiFactorAuthenticationModule = (function () { 4 5 var RESEND_COOLDOWN_SECONDS = 60; 6 var RESENT_MESSAGE_MS = 5000; 7 8 // Verification outcomes returned by the server. 9 var OUTCOME_VALID = 'Valid'; 10 var OUTCOME_INVALID = 'Invalid'; 11 var OUTCOME_EXPIRED = 'Expired'; 12 var OUTCOME_LOCKED = 'Locked'; 13 var OUTCOME_EMPTY = 'Empty'; 14 // Resend outcomes / layer-owned tokens. 15 var OUTCOME_SUCCESS = 'Success'; 16 var OUTCOME_THROTTLED = 'Throttled'; 17 var OUTCOME_CAP_REACHED = 'CapReached'; 18 19 var GENERIC_ERROR = 'An error occurred. Please try again.'; 20 var CAP_REACHED_MESSAGE = "You've reached the maximum number of resend attempts. Please try again later."; 21 var ERROR_BODY_FIRST = 'There was an issue generating your code. Please try again later or attempt to resend the code.'; 22 // Login + email-change (376444/376443) AC-locked first-failure copy â reproduced verbatim (QA asserts 23 // the exact text). Enrollment keeps ERROR_BODY_FIRST ("your code"). 24 var ERROR_BODY_FIRST_CONSUMER = 'There was an issue generating your verification code. Please try again later or attempt to resend the code.'; 25 var ERROR_BODY_PERSISTENT = 'There was an issue generating your code. Please try again later.'; 26 27 var resendTimer = null;
28 var resendRemaining = 0; 29 var resentHideTimer = null; 30 var generationFailureCount = 0; 31 32 // Login-mode state (376444): reuse the verify UI to gate a pre-session login. In login mode the 33 // modal opens straight at the verify step, verifies against the Login token flow, and hands back 34 // to the module's login completion on success instead of showing the enrollment "All Set" state. 35 var loginMode = false; 36 var loginOptions = null; 37 38 // Email-change-mode state (376443): reuse the verify UI to gate a fully-enrolled email change. 39 // The modal opens at a "Confirm MFA Email Change" step, generates a code to the NEW address on 40 // Continue, verifies against the EmailChange token flow, and on success the API has already 41 // persisted both the primary email and the MFA target â this module then refreshes the caller's 42 // display and shows a masked confirmation. 43 var emailChangeMode = false; 44 var emailChangeOptions = null; 45 46 // Management-page-enrollment-mode state (403182/403185): the customer stays on the same 47 // MFA Management page for the whole flow (unlike the login nudge, which hands off to a different 48 // page/session on completion), so a successful verify must refresh that page's toggle/status â 49 // see returnToAccount(). 50 var managementEnrollmentMode = false; 51 52 function loadModal() { 53 // Enrollment prompt: reset the sibling-flow modes so a prior login/email-change run can't leak. 54 loginMode = false; 55 emailChangeMode = false; 56 managementEnrollmentMode = false; 57 58 // Check if modal already exists 59 if ($('#modal_MultiFactorAuthentication').length > 0) { 60 openModal(); 61 return; 62 } 63 64 // Create host if needed 65 if ($('#mvc-modal-host').length === 0) { 66 $('body').append('<div id="mvc-modal-host"></div>'); 67 } 68 69 // Load modal HTML via AJAX 70 $.ajax({
71 url: '/SharedFeatures/MultiFactorAuthentication/GetModalHtml', 72 type: 'GET', 73 cache: false, 74 success: function (html) { 75 $('#mvc-modal-host').html(html); 76 openModal(); 77 }, 78 error: function () { 79 console.error('Failed to load MFA modal'); 80 } 81 }); 82 } 83 84 function openModal() { 85 $('#modal_MultiFactorAuthentication').modal('show'); 86 } 87 88 // Entry point for the login OTP gate (376444). The server has already generated + emailed the 89 // code and deferred session establishment; this opens the modal directly at the verify step. 90 // options.onVerified is invoked after a correct code to complete the module's login. 91 function startLoginVerification(options) { 92 loginMode = true; 93 emailChangeMode = false; 94 managementEnrollmentMode = false; 95 loginOptions = options || {}; 96 generationFailureCount = 0; 97 98 if ($('#modal_MultiFactorAuthentication').length > 0) { 99 showLoginVerifyState(); 100 openModal(); 101 return; 102 } 103 104 if ($('#mvc-modal-host').length === 0) { 105 $('body').append('<div id="mvc-modal-host"></div>'); 106 } 107 108 $.ajax({
109 url: '/SharedFeatures/MultiFactorAuthentication/GetModalHtml', 110 type: 'GET', 111 cache: false, 112 success: function (html) { 113 $('#mvc-modal-host').html(html); 114 showLoginVerifyState(); 115 openModal(); 116 }, 117 error: function () { 118 console.error('Failed to load MFA modal'); 119 } 120 }); 121 } 122 123 // Shows only the "Verify Your Email" code-entry step for the login gate. The server already sent the 124 // first code, so the Resend button opens on a fresh 60s cooldown (377862) â matching the server's 125 // flat-60s throttle from the initial send. 126 function showLoginVerifyState() { 127 $('#mfa-step-1, #mfa-step-2, #mfa-step-3, #mfa-step-5, #mfa-step-error').hide(); 128 $('#mfa-step-4').show(); 129 unlockCodeInput(); 130 restrictCodeInputToDigitsOnly(); 131 $('#mfa-code-input').val('').focus(); 132 $('#mfa-code-error').hide().text(''); 133 $('#mfa-resend-status').hide(); 134 $('#mfa-verify-target').hide(); 135 startResendCountdown(RESEND_COOLDOWN_SECONDS); 136 } 137 138 // Ensures the modal partial is in the DOM, then runs onReady. Shared by the flows that open the 139 // modal at a non-default step (login gate, email-change). onError (optional) runs instead of onReady 140 // if the modal HTML itself fails to load, so a caller can surface that failure instead of it only 141 // being logged. 142 function ensureModalLoaded(onReady, onError) { 143 if ($('#modal_MultiFactorAuthentication').length > 0) { 144 onReady(); 145 return; 146 } 147 148 if ($('#mvc-modal-host').length === 0) { 149 $('body').append('<div id="mvc-modal-host"></div>'); 150 } 151 152 $.ajax({
153 url: '/SharedFeatures/MultiFactorAuthentication/GetModalHtml', 154 type: 'GET', 155 cache: false, 156 success: function (html) { 157 $('#mvc-modal-host').html(html); 158 onReady(); 159 }, 160 error: function () { 161 console.error('Failed to load MFA modal'); 162 if (typeof onError === 'function') { 163 onError(); 164 } 165 } 166 }); 167 } 168 169 // Entry point for the Management-page toggle (403182). The toggle itself is the opt-in, so this 170 // begins enrollment (writes the Pending row the 403185 verify requires) and opens the modal straight 171 // at "Select Your Verification Method" -- skipping the login-flow's introductory step (mfa-step-1), 172 // which would be a redundant "Set Up Now?" prompt for a customer who just flipped the toggle on. 173 // options.onStarted runs once the modal is open (so the caller can re-enable its toggle); 174 // options.onBeginEnrollmentError runs if the modal fails to load or the Pending-row write fails. 175 function startManagementEnrollment(options) { 176 loginMode = false; 177 emailChangeMode = false; 178 managementEnrollmentMode = true; 179 generationFailureCount = 0; 180 var opts = options || {}; 181 182 function fail() { 183 if (typeof opts.onBeginEnrollmentError === 'function') { 184 opts.onBeginEnrollmentError(); 185 } 186 } 187 188 ensureModalLoaded(function () { 189 $.ajax({
190 url: '/SharedFeatures/MultiFactorAuthentication/SetMultiFactorAuthenticationSetting', 191 type: 'POST', 192 data: JSON.stringify({ skipSetup: false }), 193 contentType: 'application/json; charset=utf-8', 194 cache: false, 195 success: function (response) { 196 if (response && response.success) { 197 showManagementMethodSelectionState(); 198 if (typeof opts.onStarted === 'function') { 199 opts.onStarted(); 200 } 201 } else { 202 fail(); 203 } 204 }, 205 error: fail 206 }); 207 }, fail); 208 } 209 210 function showManagementMethodSelectionState() { 211 hideAllSteps(); 212 $('#mfa-step-2').show(); 213 $('#mfa-step-2-title').focus(); 214 openModal(); 215 } 216 217 // Entry point for the MFA email-change gate (376443). options.newEmail is the address the customer 218 // just entered; options.onVerified(confirmedEmail) refreshes the caller's email display after the 219 // change is persisted. Opens the modal at the "Confirm MFA Email Change" step. 220 function startEmailChangeVerification(options) { 221 emailChangeMode = true; 222 loginMode = false; 223 managementEnrollmentMode = false; 224 emailChangeOptions = options || {}; 225 generationFailureCount = 0; 226 227 ensureModalLoaded(function () { 228 showConfirmEmailChangeState(); 229 openModal(); 230 }); 231 } 232 233 function showConfirmEmailChangeState() { 234 hideAllSteps(); 235 $('#mfa-confirm-email-error').hide().text(''); 236 $('#mfa-confirm-continue-btn').prop('disabled', false); 237 $('#mfa-step-confirm-email').show(); 238 $('#mfa-step-confirm-email-title').focus(); 239 } 240 241 // Continue on the confirm step: generate + send the code to the NEW email, then show the verify step. 242 function confirmEmailChange() { 243 var newEmail = emailChangeOptions ? emailChangeOptions.newEmail : null; 244 var $btn = $('#mfa-confirm-continue-btn').prop('disabled', true); 245 $('#mfa-confirm-email-error').hide().text(''); 246 247 $.ajax({
248 url: '/SharedFeatures/MultiFactorAuthentication/GenerateAndSendEmailChangeOtp', 249 type: 'POST', 250 data: JSON.stringify({ newEmail: newEmail }), 251 contentType: 'application/json', 252 success: function (response) { 253 $btn.prop('disabled', false); 254 if (response.success) { 255 enterEmailChangeVerifyState(newEmail); 256 } else { 257 var message = (response.errors && response.errors.length > 0) ? response.errors[0] : GENERIC_ERROR; 258 $('#mfa-confirm-email-error').text(message).show(); 259 } 260 }, 261 error: function () { 262 $btn.prop('disabled', false); 263 $('#mfa-confirm-email-error').text(GENERIC_ERROR).show(); 264 } 265 }); 266 } 267 268 function enterEmailChangeVerifyState(newEmail) { 269 hideAllSteps(); 270 $('#mfa-step-4').show(); 271 unlockCodeInput(); 272 restrictCodeInputToDigitsOnly(); 273 $('#mfa-code-input').val('').focus(); 274 $('#mfa-code-error').hide().text(''); 275 $('#mfa-resend-status').hide(); 276 // Show the full (unmasked) new target so a local-part typo is catchable before verifying. 277 $('#mfa-verify-target-email').text(newEmail); 278 $('#mfa-verify-target').show(); 279 // The code was just sent, so the Resend button opens on a fresh 60s cooldown (377862). 280 startResendCountdown(RESEND_COOLDOWN_SECONDS); 281 } 282 283 // The email-change code verified server-side; the API persisted both writes atomically. Refresh 284 // the caller's email display, then show the masked confirmation. 285 function completeEmailChange() { 286 var confirmedEmail = emailChangeOptions ? emailChangeOptions.newEmail : ''; 287 if (emailChangeOptions && typeof emailChangeOptions.onVerified === 'function') { 288 emailChangeOptions.onVerified(confirmedEmail); 289 } 290 showEmailChangeSuccess(confirmedEmail); 291 } 292 293 function showEmailChangeSuccess(newEmail) { 294 stopResendCountdown(); 295 hideAllSteps(); 296 // Mask the address now that the change is confirmed. 297 $('#mfa-confirm-success-email').text(maskEmail(newEmail)); 298 $('#mfa-step-confirm-success').show(); 299 $('#mfa-step-confirm-success-title').focus(); 300 } 301 302 // Hides every modal step so a flow can show only the one it needs. 303 function hideAllSteps() { 304 $('#mfa-step-confirm-email, #mfa-step-1, #mfa-step-2, #mfa-step-3, #mfa-step-4, #mfa-step-5, #mfa-step-error, #mfa-step-confirm-success').hide(); 305 } 306 307 // Mirrors the server-side MaskEmail: first local char + asterisks + domain. 308 function maskEmail(email) { 309 if (!email) { 310 return ''; 311 } 312 var at = email.indexOf('@'); 313 if (at < 1) { 314 return email; 315 } 316 if (at === 1) { 317 return '*' + email.substring(at); 318 } 319 var local = email.substring(0, at); 320 return local.charAt(0) + new Array(local.length).join('*') + email.substring(at); 321 } 322 323 function save(skipSetup) { 324 $.ajax({
325 type: "POST", 326 url: "/SharedFeatures/MultiFactorAuthentication/SetMultiFactorAuthenticationSetting", 327 data: JSON.stringify({ 328 skipSetup: skipSetup 329 }), 330 contentType: "application/json; charset=utf-8", 331 dataType: "json", 332 cache: false, 333 success: function (response) { 334 if (response.success === true) { 335 if (skipSetup) { 336 $('#modal_MultiFactorAuthentication').modal('hide'); 337 } else { 338 $('#mfa-step-1').hide(); 339 $('#mfa-step-2').show(); 340 $('#mfa-step-2-title').focus(); 341 } 342 } else { 343 alert(response.message || 'Failed to save multi-factor authentication setting'); 344 } 345 }, 346 error: function (x, e) { 347 console.error("Error saving multi-factor authentication setting", e); 348 alert('An error occurred. Please try again.'); 349 } 350 }); 351 } 352 353 function selectEmailMethod() { 354 $.ajax({
355 url: '/SharedFeatures/MultiFactorAuthentication/GetEmailSetupInfo', 356 type: 'GET', 357 success: function (response) { 358 if (response.hasEmailOnFile) { 359 $('#mfa-masked-email').text(response.maskedEmail); 360 $('#mfa-step-3-has-email').show(); 361 $('#mfa-step-3-no-email').hide(); 362 } else { 363 $('#mfa-step-3-has-email').hide(); 364 $('#mfa-step-3-no-email').show(); 365 } 366 $('#mfa-step-2').hide(); 367 $('#mfa-step-3').show(); 368 $('#mfa-step-3-title').focus(); 369 }, 370 error: function () { 371 $('#mfa-step-3-has-email').hide(); 372 $('#mfa-step-3-no-email').show(); 373 $('#mfa-step-2').hide(); 374 $('#mfa-step-3').show(); 375 $('#mfa-step-3-title').focus(); 376 } 377 }); 378 } 379 380 function sendCode() { 381 var email = null; 382 383 if ($('#mfa-step-3-no-email').is(':visible')) { 384 email = $('#mfa-email-input').val().trim(); 385 if (!isValidEmail(email)) { 386 $('#mfa-email-error').text('Please enter a valid email address.').show(); 387 return; 388 } 389 $('#mfa-email-error').hide(); 390 } 391
392 var $btn = $('#mfa-send-code-btn'); 393 $btn.prop('disabled', true) 394 .html('Generating Code <span class="glyphicon glyphicon-refresh spinning" aria-hidden="true"></span>'); 395 396 // A manual send starts a fresh generation attempt sequence. 397 generationFailureCount = 0; 398 399 $.ajax({ 400 url: '/SharedFeatures/MultiFactorAuthentication/GenerateAndSendOtp', 401 type: 'POST', 402 data: JSON.stringify({ email: email }), 403 contentType: 'application/json', 404 success: function (response) { 405 $btn.prop('disabled', false).text('Send Code'); 406 if (response.success) { 407 enterVerifyState(); 408 } else { 409 showGenerationError(); 410 } 411 }, 412 error: function () { 413 $btn.prop('disabled', false).text('Send Code'); 414 showGenerationError(); 415 } 416 }); 417 } 418 419 // Shows the "Verify Your Email" code-entry state with a fresh 60s resend countdown. 420 function enterVerifyState() { 421 $('#mfa-step-3').hide(); 422 $('#mfa-step-error').hide(); 423 $('#mfa-step-4').show(); 424 unlockCodeInput(); 425 restrictCodeInputToDigitsOnly(); 426 $('#mfa-code-input').val('').focus(); 427 $('#mfa-code-error').hide().text(''); 428 $('#mfa-resend-status').hide(); 429 $('#mfa-verify-target').hide(); 430 startResendCountdown(RESEND_COOLDOWN_SECONDS); 431 } 432 433 // Returns to the flow-appropriate verify state after a recovery resend: email-change re-shows the 434 // target address; login and enrollment use the standard state. 435 function enterVerifyStateForFlow() { 436 if (emailChangeMode) { 437 enterEmailChangeVerifyState(emailChangeOptions ? emailChangeOptions.newEmail : ''); 438 } else { 439 enterVerifyState(); 440 } 441 } 442 443 function verifyCode() { 444 var code = $('#mfa-code-input').val().trim(); 445 446 if (code === '') { 447 showCodeError('Please insert your verification code.'); 448 return; 449 } 450 451 if (!/^\d+$/.test(code)) { 452 showCodeError('Please enter numbers only.'); 453 return; 454 } 455 456 var $btn = $('#mfa-verify-btn'); 457 $btn.prop('disabled', true); 458 $('#mfa-resend-status').hide(); 459 460 var verifyUrl = '/SharedFeatures/MultiFactorAuthentication/VerifyOtp'; 461 if (loginMode) { 462 verifyUrl = '/SharedFeatures/MultiFactorAuthentication/VerifyLoginOtp'; 463 } else if (emailChangeMode) { 464 verifyUrl = '/SharedFeatures/MultiFactorAuthentication/VerifyEmailChangeOtp'; 465 } 466 467 $.ajax({
468 url: verifyUrl, 469 type: 'POST', 470 data: JSON.stringify({ code: code }), 471 contentType: 'application/json', 472 success: function (response) { 473 $btn.prop('disabled', false); 474 switch (response.outcome) { 475 case OUTCOME_VALID: 476 if (loginMode) { 477 completeLogin(); 478 } else if (emailChangeMode) { 479 completeEmailChange(); 480 } else { 481 showSuccess(); 482 } 483 break; 484 case OUTCOME_INVALID: 485 showCodeError('The code is invalid. Please try again.'); 486 break; 487 case OUTCOME_EXPIRED: 488 showCodeError('The code has expired. Please send a new verification code and try again.'); 489 break; 490 case OUTCOME_LOCKED: 491 showCodeError("You've reached the maximum number of attempts. Please send a new verification code and try again."); 492 lockCodeInput(); 493 break; 494 case OUTCOME_EMPTY: 495 showCodeError('Please insert your verification code.'); 496 break; 497 default: 498 showCodeError(GENERIC_ERROR); 499 } 500 }, 501 error: function () { 502 $btn.prop('disabled', false); 503 showCodeError(GENERIC_ERROR); 504 } 505 }); 506 } 507 508 // Resend targets the endpoint for the flow the modal was opened with; each resolves the customer 509 // server-side (login from the pending-login session key, email-change/enrollment from the session). 510 function resendUrl() { 511 if (loginMode) { 512 return '/SharedFeatures/MultiFactorAuthentication/ResendLoginOtp'; 513 } 514 if (emailChangeMode) { 515 return '/SharedFeatures/MultiFactorAuthentication/ResendEmailChangeOtp'; 516 } 517 return '/SharedFeatures/MultiFactorAuthentication/ResendOtp'; 518 } 519 520 // Resend triggered from the standard verify state. 521 function resendCode() { 522 var $btn = $('#mfa-resend-btn'); 523 if ($btn.prop('disabled')) { 524 return; 525 } 526 527 // Disable immediately to prevent a double submit; the visible countdown only starts on success. 528 $btn.prop('disabled', true); 529 $('#mfa-code-error').hide().text(''); 530 531 $.ajax({
532 url: resendUrl(), 533 type: 'POST', 534 data: JSON.stringify({}), 535 contentType: 'application/json', 536 success: function (response) { 537 handleResendResponse(response, function () { 538 unlockCodeInput(); 539 showResentMessage(); 540 startResendCountdown(cooldownFrom(response)); 541 }); 542 }, 543 error: function () { 544 showGenerationError(); 545 } 546 }); 547 } 548 549 // Resend triggered from the "Error Generating Code" state. 550 function resendFromError() { 551 var $btn = $('#mfa-error-action-btn'); 552 $btn.prop('disabled', true); 553 554 $.ajax({ 555 url: resendUrl(), 556 type: 'POST', 557 data: JSON.stringify({}), 558 contentType: 'application/json', 559 success: function (response) { 560 if (response.success) { 561 generationFailureCount = 0; 562 enterVerifyStateForFlow(); 563 showResentMessage(); 564 } else if (response.outcome === OUTCOME_CAP_REACHED) { 565 $('#mfa-error-body').text(CAP_REACHED_MESSAGE); 566 $btn.prop('disabled', false); 567 } else { 568 showGenerationError(); 569 } 570 }, 571 error: function () { 572 showGenerationError(); 573 } 574 }); 575 } 576 577 // Single dispatcher for the error-state action button so its behavior can change without rebinding. 578 function errorActionClicked() { 579 if (generationFailureCount >= 2) { 580 if (loginMode || emailChangeMode) { 581 backFromError(); 582 } else { 583 proceedWithoutEnrollment(); 584 } 585 } else { 586 resendFromError(); 587 } 588 } 589 590 // Consumer-flow (login/email-change) persistent-failure Back. Nothing was persisted or logged on 591 // (376444 defers login until verify; 376443 persists only on verify), so abandon by navigating (GET) 592 // to the current page: for login that is the customer-lookup page (fields reset), for email-change 593 // the profile page (the in-progress change is dropped). 594 function backFromError() { 595 $('#modal_MultiFactorAuthentication').modal('hide'); 596 window.location.href = window.location.pathname + window.location.search; 597 } 598 599 function handleResendResponse(response, onSuccess) { 600 if (response.success) { 601 onSuccess(); 602 } else if (response.outcome === OUTCOME_CAP_REACHED) { 603 // Cap hit for this session: leave resend disabled; the customer can still verify. 604 showCodeError(CAP_REACHED_MESSAGE); 605 } else if (response.outcome === OUTCOME_THROTTLED) { 606 startResendCountdown(cooldownFrom(response)); 607 } else { 608 showGenerationError(); 609 } 610 } 611 612 function cooldownFrom(response) { 613 return (response && response.remainingCooldownSeconds > 0) 614 ? response.remainingCooldownSeconds 615 : RESEND_COOLDOWN_SECONDS; 616 } 617 618 function showSuccess() { 619 stopResendCountdown(); 620 $('#mfa-step-4').hide(); 621 $('#mfa-step-error').hide(); 622 $('#mfa-step-5').show(); 623 $('#modal_MultiFactorAuthentication').find('.close').off('click').on('click', function () { 624 returnToAccount(); 625 }); 626 $('#mfa-step-5').find('button').first().focus(); 627 } 628 629 function showGenerationError() { 630 stopResendCountdown(); 631 generationFailureCount++; 632 633 $('#mfa-step-3').hide(); 634 $('#mfa-step-4').hide(); 635 $('#mfa-step-5').hide(); 636 $('#mfa-step-error').show(); 637 638 // Login/email-change (376444/376443) use their AC-locked first-failure copy and a Back button on 639 // the second consecutive failure; enrollment keeps its copy and "Proceed Without Enrollment". 640 var isConsumerFlow = loginMode || emailChangeMode; 641 var $btn = $('#mfa-error-action-btn').prop('disabled', false); 642 if (generationFailureCount >= 2) { 643 $('#mfa-error-body').text(ERROR_BODY_PERSISTENT); 644 if (isConsumerFlow) { 645 $btn.text('Back').attr('aria-label', 'Go back'); 646 } else { 647 $btn.text('Proceed Without Enrollment').attr('aria-label', 'Proceed without enrolling for multi-factor authentication'); 648 } 649 } else { 650 $('#mfa-error-body').text(isConsumerFlow ? ERROR_BODY_FIRST_CONSUMER : ER
650ROR_BODY_FIRST); 651 $btn.text('Resend Code').attr('aria-label', 'Resend verification code'); 652 } 653 $('#mfa-step-error-title').focus(); 654 } 655 656 function proceedWithoutEnrollment() { 657 // Skip is for this session only; enrollment stays pending so future logins re-prompt. 658 $('#modal_MultiFactorAuthentication').modal('hide'); 659 } 660 661 function startResendCountdown(seconds) { 662 stopResendCountdown(); 663 var $btn = $('#mfa-resend-btn'); 664 resendRemaining = seconds; 665 $btn.prop('disabled', true).text('Resend Code (' + resendRemaining + 's)'); 666 resendTimer = setInterval(function () { 667 resendRemaining--; 668 if (resendRemaining <= 0) { 669 stopResendCountdown(); 670 $btn.prop('disabled', false).text('Resend Code'); 671 } else { 672 $btn.text('Resend Code (' + resendRemaining + 's)'); 673 } 674 }, 1000); 675 } 676 677 function stopResendCountdown() { 678 if (resendTimer) { 679 clearInterval(resendTimer); 680 resendTimer = null; 681 } 682 } 683 684 function showResentMessage() { 685 var $status = $('#mfa-resend-status').show(); 686 if (resentHideTimer) { 687 clearTimeout(resentHideTimer); 688 } 689 resentHideTimer = setTimeout(function () { 690 $status.hide(); 691 }, RESENT_MESSAGE_MS); 692 } 693 694 function showCodeError(message) { 695 $('#mfa-resend-status').hide(); 696 $('#mfa-code-error').text(message).show(); 697 } 698 699 function lockCodeInput() { 700 $('#mfa-code-input').prop('disabled', true); 701 $('#mfa-verify-btn').prop('disabled', true); 702 } 703 704 function unlockCodeInput() { 705 $('#mfa-code-input').prop('disabled', false); 706 $('#mfa-verify-btn').prop('disabled', false); 707 } 708 709 // The input's pattern/inputmode attributes don't block non-numeric characters on their own, since 710 // Verify never triggers a native form submit (constraint validation only runs there) â so strip 711 // non-digits as the user types or pastes. Re-bound each time step 4 is (re)shown since the modal 712 // markup is reloaded fresh via AJAX for some entry flows. Namespaced so re-binding never clobbers 713 // an unrelated handler on this element. 714 function restrictCodeInputToDigitsOnly() { 715 $('#mfa-code-input').off('input.mfaDigitsOnly').on('input.mfaDigitsOnly', function () { 716 var digitsOnly = $(this).val().replace(/\D/g, '').slice(0, 6); 717 if (digitsOnly !== $(this).val()) { 718 $(this).val(digitsOnly); 719 } 720 }); 721 } 722 723 function returnToAccount() { 724 $('#modal_MultiFactorAuthentication').modal('hide'); 725 726 // 403185: unlike the login-nudge flow (which hands off to a different page/session on 727 // completion), Management-page enrollment finishes on the same page whose toggle/status text 728 // are still showing the pre-enrollment (disabled) state. Reload so they reflect the 729 // now-Enrolled+Active record from the server, mirroring the reload SetMfaActiveState's success 730 // handler already does on this same page. 731 if (managementEnrollmentMode) { 732 window.location.reload(); 733 } 734 } 735 736 // Login gate (376444): the code verified server-side. Hand back to the module's login completion 737 // (Reservations posts back to establish the session; Licensing/Vessel run the Login AJAX). The 738 // page transitions on success; closing the modal instead leaves the user signed out. 739 function completeLogin() { 740 if (loginOptions && typeof loginOptions.onVerified === 'function') { 741 loginOptions.onVerified(); 742 } 743 } 744 745 function isValidEmail(email) { 746 var re = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; 747 return re.test(email); 748 } 749 750 return { 751 loadModal: loadModal, 752 openModal: openModal, 753 startLoginVerification: startLoginVerification, 754 startManagementEnrollment: startManagementEnrollment, 755 startEmailChangeVerification: startEmailChangeVerification, 756 confirmEmailChange: confirmEmailChange, 757 save: save, 758 selectEmailMethod: selectEmailMethod, 759 sendCode: sendCode,
760 verifyCode: verifyCode, 761 resendCode: resendCode, 762 resendFromError: resendFromError, 763 errorActionClicked: errorActionClicked, 764 proceedWithoutEnrollment: proceedWithoutEnrollment, 765 returnToAccount: returnToAccount, 766 isValidEmail: isValidEmail 767 }; 768})();
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.