Changed 2FA page to input 6 characters and auto submit.

This commit is contained in:
Nick Beckley 2026-06-26 19:32:55 +01:00
parent 18a14bbd01
commit 44d7253367
4 changed files with 218 additions and 12 deletions

View File

@ -11,29 +11,120 @@
</div>
</div>
<section class="edit-panel">
<form asp-action="TwoFactor" method="post" class="asset-create-form">
<section class="edit-panel two-factor-card">
<form asp-action="TwoFactor" method="post" class="asset-create-form two-factor-body" data-two-factor-form>
<input asp-for="ReturnUrl" type="hidden" />
<input asp-for="Code" type="hidden" data-two-factor-code />
<div asp-validation-summary="ModelOnly" class="alert alert-warning"></div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label" asp-for="Code">Authenticator code</label>
<input class="form-control" asp-for="Code" autocomplete="one-time-code" />
<div class="two-factor-code-group">
<label class="form-label two-factor-label" for="twoFactorDigit1">Authenticator code</label>
<div class="two-factor-inputs" role="group" aria-label="Authenticator code">
@for (var i = 1; i <= 6; i++)
{
<input class="form-control two-factor-digit"
id="twoFactorDigit@(i)"
type="text"
inputmode="numeric"
pattern="[0-9]*"
maxlength="1"
autocomplete="one-time-code"
aria-label="Digit @i of 6"
data-two-factor-digit />
}
</div>
<span class="text-danger" asp-validation-for="Code"></span>
</div>
</div>
<div class="button-row mt-3">
<div class="two-factor-actions">
<button class="btn btn-primary" type="submit">Continue</button>
<a class="btn btn-outline-secondary" asp-action="Login">Back to Login</a>
</div>
<p class="mt-3 mb-0">
<a asp-action="TwoFactorRecovery" asp-route-returnUrl="@Model.ReturnUrl">Can't access your authenticator app? Use a recovery code.</a>
<p class="two-factor-help">
<span>Can't access your authenticator app?</span>
<a asp-action="TwoFactorRecovery" asp-route-returnUrl="@Model.ReturnUrl">Use a recovery code.</a>
</p>
</form>
</section>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
<script>
(() => {
const form = document.querySelector("[data-two-factor-form]");
if (!form) {
return;
}
const hiddenCode = form.querySelector("[data-two-factor-code]");
const digits = Array.from(form.querySelectorAll("[data-two-factor-digit]"));
let hasSubmitted = false;
const codeValue = () => digits.map(input => input.value).join("");
const syncCode = () => {
hiddenCode.value = codeValue();
};
const isComplete = () => digits.every(input => input.value.length === 1);
const submitIfComplete = () => {
syncCode();
if (isComplete() && !hasSubmitted) {
hasSubmitted = true;
form.requestSubmit();
}
};
const fillFrom = (startIndex, value) => {
const numbers = value.replace(/\D/g, "").slice(0, digits.length - startIndex);
numbers.split("").forEach((number, offset) => {
digits[startIndex + offset].value = number;
});
const nextIndex = Math.min(startIndex + numbers.length, digits.length - 1);
digits[nextIndex].focus();
submitIfComplete();
};
digits.forEach((input, index) => {
input.addEventListener("input", () => {
if (input.value.length > 1) {
fillFrom(index, input.value);
return;
}
input.value = input.value.replace(/\D/g, "");
syncCode();
if (input.value && index < digits.length - 1) {
digits[index + 1].focus();
}
submitIfComplete();
});
input.addEventListener("keydown", event => {
if (event.key === "Backspace" && !input.value && index > 0) {
digits[index - 1].focus();
digits[index - 1].value = "";
syncCode();
}
});
input.addEventListener("paste", event => {
event.preventDefault();
fillFrom(index, event.clipboardData.getData("text"));
});
input.addEventListener("focus", () => input.select());
});
form.addEventListener("submit", () => {
syncCode();
});
digits.forEach(input => {
input.value = "";
});
syncCode();
digits[0].focus();
})();
</script>
}

View File

@ -205,7 +205,7 @@
<div class="marketing-container">
<div class="section-heading">
<p class="marketing-eyebrow">Loved by story-obsessed writers</p>
<h2>Authors use PlotDirector to feel oriented again.</h2>
<h2>Authors use PlotDirector to feel in control again.</h2>
</div>
<div class="testimonial-grid">
<figure>

View File

@ -896,6 +896,121 @@ a:focus-visible {
outline: none;
}
.two-factor-card {
max-width: 560px;
margin: 0 auto;
padding: clamp(1.75rem, 4vw, 2.5rem);
}
.two-factor-body {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 1.5rem;
border-top: 0;
padding-top: 0;
}
.two-factor-body .alert {
width: 100%;
margin-bottom: 0;
}
.two-factor-code-group {
display: grid;
justify-items: center;
gap: 0.75rem;
width: 100%;
}
.two-factor-label {
margin-bottom: 0;
font-weight: 700;
color: var(--plotline-text);
}
.two-factor-inputs {
display: flex;
justify-content: center;
gap: 0.75rem;
width: 100%;
}
.two-factor-digit {
width: 3.5rem;
height: 3.5rem;
padding: 0;
text-align: center;
font-size: 1.6rem;
font-weight: 760;
line-height: 1;
border-radius: 0.75rem;
border-color: rgba(122, 77, 44, 0.28);
background: var(--plotline-surface-elevated);
color: var(--plotline-text);
}
.two-factor-digit:focus {
border-color: var(--plotline-accent);
box-shadow:
0 0 0 0.22rem rgba(122, 77, 44, 0.2),
0 10px 22px rgba(62, 45, 28, 0.1);
}
.two-factor-actions {
display: flex;
justify-content: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.two-factor-actions .btn {
min-width: 8.75rem;
}
.two-factor-help {
display: grid;
justify-items: center;
gap: 0.2rem;
margin: 0;
font-size: 0.95rem;
color: var(--plotline-text-muted);
}
.two-factor-help a {
font-weight: 650;
}
.two-factor-code-group .text-danger {
min-height: 1.4rem;
}
@media (max-width: 576px) {
.two-factor-card {
padding: 1.35rem;
}
.two-factor-inputs {
gap: 0.4rem;
}
.two-factor-digit {
width: 3rem;
height: 3rem;
font-size: 1.45rem;
}
.two-factor-actions {
flex-direction: column;
width: 100%;
}
.two-factor-actions .btn {
width: 100%;
}
}
.plotline-confirm-modal {
background: #fffaf2;
border: 1px solid rgba(84, 68, 48, 0.16);

File diff suppressed because one or more lines are too long