Move passkey enrollment prompt to post-login dedicated page

After password login, users are routed through /Passkey/EnrollPrompt
before reaching the dashboard. The page shows an Enable / Maybe later
choice using the auth layout for a clean full-screen experience.
Users who already have a passkey are skipped past instantly.

Removes the floating bottom-right card from _Layout — the dedicated
page is a better UX touchpoint (one moment, right after login, rather
than a floating card on every page).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 16:41:01 -04:00
parent 92f71f62d0
commit edce8e8c4a
6 changed files with 144 additions and 84 deletions
@@ -0,0 +1,52 @@
document.addEventListener('DOMContentLoaded', async () => {
const enableBtn = document.getElementById('pk-enable-btn');
const btnLabel = document.getElementById('pk-btn-label');
const statusEl = document.getElementById('pk-status');
const promptStep = document.getElementById('prompt-step');
const successStep = document.getElementById('success-step');
if (!enableBtn) return;
// Set platform-specific label
const label = passkeyLabel();
if (btnLabel) btnLabel.textContent = `Enable ${label.replace('Use ', '')}`;
const supported = await passkeySupported();
if (!supported) {
enableBtn.disabled = true;
enableBtn.textContent = 'Not supported on this browser';
if (statusEl) {
statusEl.textContent = 'Your browser does not support biometric login. You can enable it later from Settings → Passkeys & Biometrics on a supported device.';
statusEl.className = 'small mb-3 text-muted';
}
return;
}
enableBtn.addEventListener('click', async () => {
enableBtn.disabled = true;
enableBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Follow the prompt on your device…';
if (statusEl) { statusEl.textContent = ''; }
const ua = navigator.userAgent;
const deviceName = /iphone/i.test(ua) ? 'iPhone'
: /ipad/i.test(ua) ? 'iPad'
: /android/i.test(ua) ? 'Android device'
: /macintosh/i.test(ua) ? 'Mac'
: /windows/i.test(ua) ? 'Windows PC'
: 'This device';
const result = await registerPasskey(deviceName);
if (result.success) {
promptStep.classList.add('d-none');
successStep.classList.remove('d-none');
} else {
enableBtn.disabled = false;
enableBtn.innerHTML = `<i class="bi bi-fingerprint me-2"></i>Enable ${label.replace('Use ', '')}`;
if (statusEl) {
statusEl.textContent = result.error || 'Setup failed. Please try again.';
statusEl.className = 'small mb-3 text-danger';
}
}
});
});
@@ -272,60 +272,3 @@ document.addEventListener('DOMContentLoaded', async () => {
});
});
// ─── Post-login prompt wiring (layout) ───────────────────────────────────────
document.addEventListener('DOMContentLoaded', async () => {
const prompt = document.getElementById('passkey-setup-prompt');
if (!prompt) return;
const supported = await passkeySupported();
if (!supported) { prompt.remove(); return; }
const label = passkeyLabel();
const enableBtn = document.getElementById('passkey-enable-btn');
if (enableBtn) enableBtn.innerHTML = `<i class="bi bi-fingerprint me-1"></i>Enable ${label.replace('Use ', '')}`;
prompt.classList.remove('d-none');
const dismissBtn = document.getElementById('passkey-dismiss-btn');
const statusEl = document.getElementById('passkey-setup-status');
enableBtn?.addEventListener('click', async () => {
enableBtn.disabled = true;
if (statusEl) statusEl.textContent = 'Follow the prompt on your device…';
const ua = navigator.userAgent;
const deviceName = /iphone/i.test(ua) ? 'iPhone'
: /ipad/i.test(ua) ? 'iPad'
: /android/i.test(ua) ? 'Android device'
: /macintosh/i.test(ua) ? 'Mac'
: /windows/i.test(ua) ? 'Windows PC'
: 'This device';
const result = await registerPasskey(deviceName);
if (result.success) {
if (statusEl) {
statusEl.textContent = `${label.replace('Use ', '')} enabled for this device!`;
statusEl.className = 'small mb-0 text-success';
}
enableBtn.classList.add('d-none');
if (dismissBtn) dismissBtn.textContent = 'Close';
setTimeout(() => prompt.classList.add('d-none'), 3000);
} else {
enableBtn.disabled = false;
enableBtn.innerHTML = `<i class="bi bi-fingerprint me-1"></i>Enable ${label.replace('Use ', '')}`;
if (statusEl) statusEl.textContent = result.error || 'Setup failed. Try again.';
}
});
dismissBtn?.addEventListener('click', () => {
prompt.classList.add('d-none');
// Remember dismissal for this session so it doesn't re-appear on every page load
sessionStorage.setItem('passkey-prompt-dismissed', '1');
});
if (sessionStorage.getItem('passkey-prompt-dismissed')) {
prompt.classList.add('d-none');
}
});