PageSourceSearch

https://license.gooutdoorsgeorgia.com/Scripts/MultiFactorAuthentication.js?v=20260915032820

js gooutdoorsgeorgia.com collected 2026-09-29 07:50:34 UTC 31,040 bytes, 768 lines download raw bytes

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 &nbsp;<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.