chore: remove deprecated templ files and update auth routes

BREAKING: migrates to templ from html templates in login, register and welcome pages
This commit is contained in:
2026-01-05 13:44:02 -05:00
parent 901b1116be
commit 70302c27ea
4 changed files with 37 additions and 1777 deletions

View File

@@ -1,561 +0,0 @@
<!DOCTYPE html>
<!--
================================================================================
TEMPL MIGRATION GUIDE: login.html → views/login.templ
================================================================================
PAGE OVERVIEW:
- WebAuthn sign-in page with multiple authentication methods
- Supports passkey (biometrics), security key, and QR code authentication
- Includes conditional UI for passkey autofill in username field
- Account recovery flow with recovery key or QR code verification
- VIEWPORT: Optimized for popup/webview (360×540 to 480×680)
VIEWPORT CONSTRAINTS (Auth Popup/Webview):
- Target sizes: 360×540 (small), 420×600 (medium), 480×680 (large)
- Card max-width: 45ch (~360px) fits all viewport sizes
- Auth method cards stack vertically, touch-friendly (min 44px height)
- QR code step should center and fit within 360px width
- Recovery flow inputs sized for constrained width
MAIN TEMPL COMPONENT:
templ LoginPage(step string, capabilities DeviceCapabilities) {
@layouts.CenteredCard("Sign In - Sonr Motr Wallet") {
switch step {
case "1":
@LoginStep1(capabilities)
case "qr":
@QRAuthStep()
case "recovery":
@RecoveryStep()
case "success":
@SuccessStep("Welcome Back!", "/dashboard")
}
}
}
HTMX INTEGRATION:
- Replace onclick="goToStep('recovery')" with hx-get="/login?step=recovery" hx-target="#step-content"
- Replace onclick="signIn()" with hx-post="/api/auth/signin" hx-target="#step-content"
- Auth method selection: hx-post="/login/select-method" hx-vals='{"method":"passkey"}'
- QR polling: hx-get="/api/auth/qr-status" hx-trigger="every 2s" hx-target="#qr-status"
- Conditional UI: autocomplete="username webauthn" triggers browser passkey autofill
SUB-COMPONENTS TO EXTRACT:
- AuthMethodCard(method string, icon string, title string, desc string, disabled bool)
- QRCodeSection(value string, label string, fallbackText string)
- RecoveryForm(method string) // "recovery-key" or "qr-code"
- SuccessStep(title string, redirectUrl string)
STATE/PROPS:
type DeviceCapabilities struct {
PlatformAuth bool // Face ID, Touch ID, Windows Hello
CrossPlatform bool // Security keys (YubiKey)
ConditionalUI bool // Passkey autofill support
}
type LoginState struct {
Step string
SelectedMethod string // "passkey", "security-key", "qr-code"
Username string
Error string
}
HTMX PATTERNS:
// Auth method card with HTMX selection
<div class="auth-method-card"
hx-post="/login/select-method"
hx-vals='{"method":"passkey"}'
hx-target="#btn-signin"
hx-swap="outerHTML">
// Sign in button enabled via HTMX swap
<wa-button id="btn-signin" variant="brand"
hx-post="/api/auth/signin"
hx-include="[name='username'],[name='method']"
hx-target="#step-content">
// QR code polling for cross-device auth
<div id="qr-status"
hx-get="/api/auth/qr-status?session=xyz789"
hx-trigger="every 2s"
hx-target="this">
================================================================================
-->
<html lang="en" class="wa-cloak">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sign In - Sonr</title>
<script src="https://cdn.sonr.org/wa/autoloader.js"></script>
<style>
:root {
--wa-color-primary: #17c2ff;
}
html, body {
min-height: 100%;
padding: 0;
margin: 0;
}
.main-centered {
display: flex;
justify-content: center;
align-items: center;
min-height: 100%;
padding: var(--wa-space-l);
box-sizing: border-box;
}
.main-centered wa-card {
width: 100%;
max-width: 45ch;
}
.step {
display: none;
}
.step.active {
display: block;
}
.auth-method-card {
display: flex;
align-items: center;
gap: var(--wa-space-m);
padding: var(--wa-space-m);
border-radius: var(--wa-radius-m);
background: var(--wa-color-surface-alt);
cursor: pointer;
border: 2px solid transparent;
transition: border-color 0.2s, background 0.2s;
}
.auth-method-card:hover {
background: var(--wa-color-surface-alt-hover);
}
.auth-method-card.selected {
border-color: var(--wa-color-primary);
}
.auth-method-card.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.auth-method-card wa-icon {
font-size: 2rem;
color: var(--wa-color-primary);
}
.auth-method-info {
flex: 1;
}
.auth-method-info h4 {
margin: 0 0 var(--wa-space-2xs) 0;
}
.auth-method-info p {
margin: 0;
color: var(--wa-color-neutral-600);
}
/* Viewport constraints for popup/webview (360-480px width) */
@media (max-width: 400px) {
.main-centered {
padding: var(--wa-space-m);
}
.main-centered wa-card {
max-width: 100%;
}
.auth-method-card {
padding: var(--wa-space-s);
gap: var(--wa-space-s);
}
.auth-method-card wa-icon {
font-size: 1.5rem;
}
}
@media (max-height: 600px) {
.auth-method-card {
padding: var(--wa-space-s);
}
}
</style>
</head>
<body>
<wa-page>
<main class="main-centered">
<wa-card>
<!-- Step 1: Sign In Options -->
<div class="step active" data-step="1">
<div class="wa-stack wa-gap-l">
<div class="wa-stack wa-gap-2xs">
<h2 class="wa-heading-l">Welcome Back</h2>
<p class="wa-caption-s">How would you like to sign in?</p>
</div>
<wa-input
name="username"
type="text"
label="Username"
placeholder="Enter your username"
id="login-username"
autocomplete="username webauthn"
>
<wa-icon slot="end" variant="regular" name="user"></wa-icon>
</wa-input>
<div class="wa-stack wa-gap-s" id="auth-methods">
<div class="auth-method-card" data-method="passkey" id="method-passkey">
<wa-icon family="duotone" name="fingerprint"></wa-icon>
<div class="auth-method-info">
<h4 class="wa-label-m">Face or Fingerprint</h4>
<p class="wa-caption-s">Use your face, fingerprint, or device PIN</p>
</div>
<wa-spinner style="--size: 1.25rem; display: none;" id="passkey-spinner"></wa-spinner>
</div>
<div class="auth-method-card" data-method="security-key" id="method-security-key">
<wa-icon family="duotone" name="key"></wa-icon>
<div class="auth-method-info">
<h4 class="wa-label-m">Hardware Key</h4>
<p class="wa-caption-s">Use a physical security key</p>
</div>
<wa-spinner style="--size: 1.25rem; display: none;" id="security-key-spinner"></wa-spinner>
</div>
<div class="auth-method-card" data-method="qr-code" id="method-qr">
<wa-icon family="duotone" name="qrcode"></wa-icon>
<div class="auth-method-info">
<h4 class="wa-label-m">Use Another Device</h4>
<p class="wa-caption-s">Scan a QR code with your phone</p>
</div>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-cluster wa-justify-content-between wa-gap-s">
<wa-button appearance="plain" size="small" onclick="goToStep('recovery')">
Can't sign in?
</wa-button>
<wa-button variant="brand" id="btn-signin" disabled>
Sign In
<wa-icon slot="end" variant="regular" name="arrow-right"></wa-icon>
</wa-button>
</div>
</div>
</div>
<!-- Step 2: QR Code Authentication -->
<div class="step" data-step="qr">
<div class="wa-stack wa-gap-l wa-align-items-center">
<h2 class="wa-heading-l">Use Your Phone</h2>
<p class="wa-caption-s">Scan this code with your phone camera</p>
<wa-qr-code
value="https://sonr.id/auth?session=xyz789abc123"
label="Scan with your phone camera"
></wa-qr-code>
<p class="wa-caption-s">Or copy this link to your phone</p>
<wa-input
value="sonr.id/a/xyz789abc123"
disabled
style="width: 100%;"
>
<wa-copy-button slot="end" value="sonr.id/a/xyz789abc123"></wa-copy-button>
</wa-input>
<wa-divider></wa-divider>
<div class="wa-stack wa-gap-s wa-align-items-center" style="width: 100%;">
<wa-spinner id="qr-waiting-spinner"></wa-spinner>
<p class="wa-caption-s">Waiting for your phone...</p>
</div>
<wa-button appearance="outlined" variant="neutral" onclick="goToStep(1)" style="width: 100%;">
Back
</wa-button>
</div>
</div>
<!-- Step 3: Recovery -->
<div class="step" data-step="recovery">
<div class="wa-stack wa-gap-l">
<h2 class="wa-heading-l">Recover Your Account</h2>
<wa-radio-group
label="How would you like to recover?"
orientation="horizontal"
name="recovery-method"
value="recovery-key"
id="recovery-method-group"
>
<wa-radio appearance="button" value="recovery-key">Backup Code</wa-radio>
<wa-radio appearance="button" value="qr-code">Phone</wa-radio>
</wa-radio-group>
<div id="recovery-key-form">
<div class="wa-stack wa-gap-m">
<p class="wa-caption-s">
Enter the backup code you saved when you created your account.
</p>
<wa-input
name="recovery-key"
type="text"
label="Backup Code"
placeholder="rk_sec_..."
id="recovery-key-input"
>
<wa-icon slot="end" variant="regular" name="key"></wa-icon>
</wa-input>
</div>
</div>
<div id="recovery-qr-form" style="display: none;">
<div class="wa-stack wa-gap-m wa-align-items-center">
<p class="wa-caption-s">
Scan this code with your phone, then enter the code shown.
</p>
<wa-qr-code
value="https://sonr.id/recover?token=rec123xyz"
label="Scan with your phone"
></wa-qr-code>
<wa-input
type="text"
placeholder="000000"
maxlength="6"
style="text-align: center; font-size: 2rem; letter-spacing: 0.5em; max-width: 200px;"
id="recovery-code"
></wa-input>
</div>
</div>
<div class="wa-cluster wa-justify-content-end wa-gap-s">
<wa-button appearance="outlined" variant="neutral" onclick="goToStep(1)">
Back
</wa-button>
<wa-button variant="brand" id="btn-recover">
Recover
</wa-button>
</div>
</div>
</div>
<!-- Step 4: Success -->
<div class="step" data-step="success">
<div class="wa-stack wa-gap-l wa-align-items-center">
<wa-icon name="circle-check" family="duotone" style="font-size: 4rem; color: var(--wa-color-success);"></wa-icon>
<h2 class="wa-heading-l">You're In</h2>
<p class="wa-caption-s">
Signed in successfully.
</p>
<wa-button variant="brand" style="width: 100%;" onclick="window.location.href='dashboard.html'">
Go to Dashboard
<wa-icon slot="end" variant="regular" name="arrow-right"></wa-icon>
</wa-button>
</div>
</div>
<footer slot="footer" class="wa-cluster wa-justify-content-center wa-gap-m">
<span class="wa-caption-s">Don't have an account?</span>
<wa-button appearance="plain" size="small" onclick="window.location.href='register.html'">
Create Account
</wa-button>
</footer>
</wa-card>
</main>
</wa-page>
<script>
let selectedMethod = null;
let capabilities = {
platform: false,
crossPlatform: false,
conditional: false
};
async function checkCapabilities() {
if (!window.PublicKeyCredential) {
document.getElementById('method-passkey').classList.add('disabled');
document.getElementById('method-security-key').classList.add('disabled');
return;
}
try {
capabilities.platform = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
if (!capabilities.platform) {
document.getElementById('method-passkey').classList.add('disabled');
}
} catch (e) {
document.getElementById('method-passkey').classList.add('disabled');
}
capabilities.crossPlatform = true;
try {
if (PublicKeyCredential.isConditionalMediationAvailable) {
capabilities.conditional = await PublicKeyCredential.isConditionalMediationAvailable();
if (capabilities.conditional) {
startConditionalUI();
}
}
} catch (e) {}
}
async function startConditionalUI() {
try {
const challenge = new Uint8Array(32);
crypto.getRandomValues(challenge);
const credential = await navigator.credentials.get({
publicKey: {
challenge: challenge,
rpId: window.location.hostname,
userVerification: "preferred",
timeout: 300000
},
mediation: "conditional"
});
if (credential) {
console.log("Conditional UI authentication successful:", credential);
goToStep('success');
}
} catch (error) {
console.log("Conditional UI not used or failed:", error);
}
}
function selectMethod(method) {
if (method === 'passkey' && !capabilities.platform) return;
if (method === 'security-key' && !capabilities.crossPlatform) return;
document.querySelectorAll('.auth-method-card').forEach(card => {
card.classList.remove('selected');
});
const card = document.querySelector(`[data-method="${method}"]`);
if (card && !card.classList.contains('disabled')) {
card.classList.add('selected');
selectedMethod = method;
document.getElementById('btn-signin').removeAttribute('disabled');
}
}
async function signIn() {
if (!selectedMethod) return;
if (selectedMethod === 'qr-code') {
goToStep('qr');
simulateQRPolling();
return;
}
const spinner = document.getElementById(
selectedMethod === 'passkey' ? 'passkey-spinner' : 'security-key-spinner'
);
spinner.style.display = 'block';
try {
const challenge = new Uint8Array(32);
crypto.getRandomValues(challenge);
const getOptions = {
publicKey: {
challenge: challenge,
rpId: window.location.hostname,
userVerification: selectedMethod === 'passkey' ? "required" : "preferred",
timeout: 60000
}
};
if (selectedMethod === 'security-key') {
getOptions.publicKey.allowCredentials = [];
}
const credential = await navigator.credentials.get(getOptions);
console.log("Authentication successful:", credential);
goToStep('success');
} catch (error) {
console.error("Authentication failed:", error);
alert("Authentication failed. Please try again.");
} finally {
spinner.style.display = 'none';
}
}
function simulateQRPolling() {
setTimeout(() => {
const step = document.querySelector('.step[data-step="qr"]');
if (step.classList.contains('active')) {
goToStep('success');
}
}, 5000);
}
function goToStep(step) {
document.querySelectorAll('.step').forEach(el => el.classList.remove('active'));
const targetStep = document.querySelector(`.step[data-step="${step}"]`);
if (targetStep) {
targetStep.classList.add('active');
}
}
document.addEventListener('DOMContentLoaded', () => {
checkCapabilities();
document.querySelectorAll('.auth-method-card').forEach(card => {
card.addEventListener('click', () => {
selectMethod(card.dataset.method);
});
});
document.getElementById('btn-signin').addEventListener('click', signIn);
const recoveryMethodGroup = document.getElementById('recovery-method-group');
recoveryMethodGroup.addEventListener('wa-change', (e) => {
const keyForm = document.getElementById('recovery-key-form');
const qrForm = document.getElementById('recovery-qr-form');
if (e.target.value === 'recovery-key') {
keyForm.style.display = 'block';
qrForm.style.display = 'none';
} else {
keyForm.style.display = 'none';
qrForm.style.display = 'block';
}
});
document.getElementById('btn-recover').addEventListener('click', () => {
const method = recoveryMethodGroup.value;
if (method === 'recovery-key') {
const key = document.getElementById('recovery-key-input').value;
if (key.startsWith('rk_')) {
console.log("Recovery key validated:", key);
goToStep('success');
} else {
alert("Please enter a valid recovery key");
}
} else {
const code = document.getElementById('recovery-code').value;
if (code.length === 6) {
console.log("Recovery code verified:", code);
goToStep('success');
} else {
alert("Please enter a valid 6-digit code");
}
}
});
});
</script>
</body>
</html>

View File

@@ -1,680 +0,0 @@
<!DOCTYPE html>
<!--
================================================================================
TEMPL MIGRATION GUIDE: register.html → views/register.templ
================================================================================
PAGE OVERVIEW:
- WebAuthn registration flow with 3-step wizard
- Step 1: Device capability detection + auth method selection
- Step 2: Method-specific registration (passkey, security key, or QR)
- Step 3: Success with recovery key display and confirmation
- VIEWPORT: Optimized for popup/webview (360×540 to 480×680)
VIEWPORT CONSTRAINTS (Auth Popup/Webview):
- Target sizes: 360×540 (small), 420×600 (medium), 480×680 (large)
- Card max-width: 45ch (~360px) fits all viewport sizes
- Capability list items stack vertically with compact padding
- QR code centered with fallback text input below
- Recovery key inputs scrollable if needed on step 3
MAIN TEMPL COMPONENT:
templ RegisterPage(step int, method string, caps DeviceCapabilities) {
@layouts.CenteredCard("Register - Sonr Motr Wallet") {
@ProgressDots(step, 3)
switch step {
case 1:
@DeviceDetectionStep(caps)
case 2:
@RegistrationMethodStep(method)
case 3:
@RegistrationSuccessStep()
}
}
}
HTMX INTEGRATION:
- Device detection: hx-get="/api/device/capabilities" hx-trigger="load" hx-target="#capabilities"
- Method selection: hx-post="/register/select-method" hx-vals='{"method":"passkey"}'
- Step navigation: hx-get="/register?step=2&method=passkey" hx-target="#step-content"
- WebAuthn trigger: hx-post="/api/auth/register" hx-trigger="click" (JS bridge needed)
- QR verification: hx-post="/api/auth/verify-code" hx-include="[name='code']"
SUB-COMPONENTS TO EXTRACT:
- ProgressDots(currentStep int, totalSteps int)
- CapabilityItem(id string, label string, supported bool, loading bool)
- RegistrationForm(method string) // passkey, security-key, or qr-code
- RecoveryKeyDisplay(publicKey string, recoveryKey string)
- QRCodeRegistration(qrValue string, sessionToken string)
STATE/PROPS:
type DeviceCapabilities struct {
PlatformAuth bool // Biometrics available
CrossPlatform bool // Security key support
ConditionalUI bool // Passkey autofill
}
type RegisterState struct {
Step int
SelectedMethod string
Username string
DisplayName string
PublicKey string
RecoveryKey string
Error string
}
HTMX PATTERNS:
// Capability detection on page load
<div id="capabilities" hx-get="/api/device/capabilities" hx-trigger="load delay:500ms">
@CapabilityItem("platform", "Biometric Authentication", false, true)
@CapabilityItem("cross-platform", "Security Key", false, true)
@CapabilityItem("conditional", "Passkey Autofill", false, true)
</div>
// Radio group with HTMX method selection
<wa-radio-group hx-on:wa-change="htmx.ajax('POST', '/register/select-method', {values: {method: event.target.value}})">
// Registration form submission (requires JS bridge for WebAuthn)
<form hx-post="/api/auth/register"
hx-target="#step-content"
hx-indicator="#register-spinner">
// Recovery key confirmation checkbox enabling finish button
<wa-checkbox hx-on:wa-change="document.getElementById('btn-finish').disabled = !this.checked">
================================================================================
-->
<html lang="en" class="wa-cloak">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Create Account - Sonr</title>
<script src="https://cdn.sonr.org/wa/autoloader.js"></script>
<style>
:root {
--wa-color-primary: #17c2ff;
}
html, body {
min-height: 100%;
padding: 0;
margin: 0;
}
.main-centered {
display: flex;
justify-content: center;
align-items: center;
min-height: 100%;
padding: var(--wa-space-l);
box-sizing: border-box;
}
.main-centered wa-card {
width: 100%;
max-width: 45ch;
}
/* Pagination steps */
.step {
display: none;
}
.step.active {
display: block;
}
/* Device capability badges */
.capability-list {
display: flex;
flex-direction: column;
gap: var(--wa-space-s);
}
.capability-item {
display: flex;
align-items: center;
gap: var(--wa-space-s);
padding: var(--wa-space-s);
border-radius: var(--wa-radius-m);
background: var(--wa-color-surface-alt);
}
.capability-item.supported {
border-left: 3px solid var(--wa-color-success);
}
.capability-item.unsupported {
border-left: 3px solid var(--wa-color-danger);
opacity: 0.6;
}
/* Progress indicator */
.progress-steps {
display: flex;
justify-content: center;
gap: var(--wa-space-xs);
margin-bottom: var(--wa-space-l);
}
.progress-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--wa-color-neutral-300);
transition: background 0.2s;
}
.progress-dot.active {
background: var(--wa-color-primary);
}
.progress-dot.completed {
background: var(--wa-color-success);
}
/* Viewport constraints for popup/webview (360-480px width) */
@media (max-width: 400px) {
.main-centered {
padding: var(--wa-space-m);
}
.main-centered wa-card {
max-width: 100%;
}
.capability-item {
padding: var(--wa-space-xs);
gap: var(--wa-space-xs);
font-size: var(--wa-font-size-s);
}
}
@media (max-height: 600px) {
.progress-steps {
margin-bottom: var(--wa-space-s);
}
.capability-item {
padding: var(--wa-space-xs);
}
}
</style>
</head>
<body>
<wa-page>
<main class="main-centered">
<wa-card>
<div slot="header">
<div class="progress-steps">
<div class="progress-dot active" data-step="1"></div>
<div class="progress-dot" data-step="2"></div>
<div class="progress-dot" data-step="3"></div>
</div>
</div>
<!-- Step 1: Device Detection -->
<div class="step active" data-step="1">
<div class="wa-stack wa-gap-l">
<h2 class="wa-heading-l">Create Your Account</h2>
<p class="wa-caption-s">
Setting up secure sign-in...
</p>
<div class="capability-list" id="capabilities">
<div class="capability-item" id="cap-platform">
<wa-spinner style="--size: 1.5rem;"></wa-spinner>
<span>Face or Fingerprint</span>
</div>
<div class="capability-item" id="cap-cross-platform">
<wa-spinner style="--size: 1.5rem;"></wa-spinner>
<span>Hardware Security Key</span>
</div>
<div class="capability-item" id="cap-conditional">
<wa-spinner style="--size: 1.5rem;"></wa-spinner>
<span>Quick Sign-in</span>
</div>
</div>
<wa-divider></wa-divider>
<wa-radio-group
label="How do you want to sign in?"
orientation="vertical"
name="auth-method"
id="auth-method-group"
>
<wa-radio appearance="button" value="passkey" id="radio-passkey" disabled>
Use Face or Fingerprint (Recommended)
</wa-radio>
<wa-radio appearance="button" value="security-key" id="radio-security-key" disabled>
Use a Hardware Key
</wa-radio>
<wa-radio appearance="button" value="qr-code" id="radio-qr">
Use Another Device
</wa-radio>
</wa-radio-group>
<div class="wa-cluster wa-justify-content-end wa-gap-s">
<wa-button id="btn-next-1" variant="brand" disabled>
Continue
<wa-icon slot="end" variant="regular" name="arrow-right"></wa-icon>
</wa-button>
</div>
</div>
</div>
<!-- Step 2a: Passkey Registration -->
<div class="step" data-step="2" data-method="passkey">
<div class="wa-stack wa-gap-l">
<h2 class="wa-heading-l">Set Up Face or Fingerprint</h2>
<p class="wa-caption-s">
Sign in securely using your face or fingerprint.
</p>
<wa-input
name="username"
type="text"
label="Username"
placeholder="Choose a username"
required
>
<wa-icon slot="end" variant="regular" name="user"></wa-icon>
</wa-input>
<wa-input
name="display-name"
type="text"
label="Display Name"
placeholder="Your name (optional)"
>
<wa-icon slot="end" variant="regular" name="id-card"></wa-icon>
</wa-input>
<wa-alert variant="info" open>
<wa-icon slot="icon" name="fingerprint"></wa-icon>
<strong>Next: Verify your identity</strong><br>
Your device will ask for your face, fingerprint, or PIN.
</wa-alert>
<div class="wa-cluster wa-justify-content-end wa-gap-s">
<wa-button appearance="outlined" variant="neutral" onclick="goToStep(1)">
Back
</wa-button>
<wa-button variant="brand" onclick="registerPasskey()">
Continue
<wa-icon slot="end" variant="regular" name="fingerprint"></wa-icon>
</wa-button>
</div>
</div>
</div>
<!-- Step 2b: Security Key Registration -->
<div class="step" data-step="2" data-method="security-key">
<div class="wa-stack wa-gap-l">
<h2 class="wa-heading-l">Set Up Hardware Key</h2>
<p class="wa-caption-s">
Use a physical security key like YubiKey to sign in.
</p>
<wa-input
name="username"
type="text"
label="Username"
placeholder="Choose a username"
required
>
<wa-icon slot="end" variant="regular" name="user"></wa-icon>
</wa-input>
<wa-alert variant="info" open>
<wa-icon slot="icon" name="key"></wa-icon>
<strong>Plug in your security key</strong><br>
After clicking Continue, tap the button on your key when it blinks.
</wa-alert>
<div class="wa-cluster wa-justify-content-end wa-gap-s">
<wa-button appearance="outlined" variant="neutral" onclick="goToStep(1)">
Back
</wa-button>
<wa-button variant="brand" onclick="registerSecurityKey()">
Continue
<wa-icon slot="end" variant="regular" name="key"></wa-icon>
</wa-button>
</div>
</div>
</div>
<!-- Step 2c: QR Code Registration (Fallback) -->
<div class="step" data-step="2" data-method="qr-code">
<div class="wa-stack wa-gap-l wa-align-items-center">
<h2 class="wa-heading-l">Use Your Phone</h2>
<p class="wa-caption-s">Scan this code with your phone camera to continue setup</p>
<wa-qr-code
value="https://sonr.id/register?token=abc123xyz789"
label="Scan with your phone camera"
></wa-qr-code>
<p class="wa-caption-s">Or copy this link to your phone</p>
<wa-input
value="sonr.id/r/abc123xyz789"
disabled
style="width: 100%;"
>
<wa-copy-button slot="end" value="sonr.id/r/abc123xyz789"></wa-copy-button>
</wa-input>
<wa-divider></wa-divider>
<h3 class="wa-heading-m">Enter Code from Phone</h3>
<p class="wa-caption-s">After scanning, enter the 6-digit code shown on your phone.</p>
<wa-input
type="text"
placeholder="000000"
maxlength="6"
style="text-align: center; font-size: 2rem; letter-spacing: 0.5em; max-width: 200px;"
id="verification-code"
></wa-input>
<div class="wa-cluster wa-justify-content-center wa-gap-s" style="width: 100%;">
<wa-button appearance="outlined" variant="neutral" onclick="goToStep(1)">
Back
</wa-button>
<wa-button variant="brand" onclick="verifyCode()">
Verify
</wa-button>
</div>
</div>
</div>
<!-- Step 3: Success -->
<div class="step" data-step="3">
<div class="wa-stack wa-gap-l wa-align-items-center">
<wa-icon name="circle-check" family="duotone" style="font-size: 4rem; color: var(--wa-color-success);"></wa-icon>
<h2 class="wa-heading-l">You're All Set</h2>
<p class="wa-caption-s">
Your account is ready. Save your backup codes before continuing.
</p>
<wa-alert variant="warning" open>
<wa-icon slot="icon" name="shield-exclamation"></wa-icon>
<strong>Important: Save these codes now</strong><br>
If you lose your device, these codes are the only way to recover your account.
</wa-alert>
<div class="wa-stack" style="width: 100%;">
<wa-input label="Account ID" value="pk_live_a1b2c3d4e5f6g7h8i9j0" disabled>
<wa-copy-button slot="end" value="pk_live_a1b2c3d4e5f6g7h8i9j0"></wa-copy-button>
</wa-input>
<wa-input label="Backup Code" value="rk_sec_z9y8x7w6v5u4t3s2r1q0" disabled>
<wa-copy-button slot="end" value="rk_sec_z9y8x7w6v5u4t3s2r1q0"></wa-copy-button>
</wa-input>
</div>
<wa-checkbox id="confirm-saved" required>
I saved these codes somewhere safe
</wa-checkbox>
<wa-button variant="brand" id="btn-finish" disabled style="width: 100%;">
Go to Dashboard
<wa-icon slot="end" variant="regular" name="arrow-right"></wa-icon>
</wa-button>
</div>
</div>
<footer slot="footer" class="wa-cluster wa-justify-content-center wa-gap-m">
<span class="wa-caption-s">Already have an account?</span>
<wa-button appearance="plain" size="small" onclick="window.location.href='login.html'">Sign In</wa-button>
</footer>
</wa-card>
</main>
</wa-page>
<script>
// State
let currentStep = 1;
let selectedMethod = null;
let capabilities = {
platform: false,
crossPlatform: false,
conditional: false
};
// Check WebAuthn support
async function checkCapabilities() {
const capPlatform = document.getElementById('cap-platform');
const capCrossPlatform = document.getElementById('cap-cross-platform');
const capConditional = document.getElementById('cap-conditional');
// Check if WebAuthn is available at all
if (!window.PublicKeyCredential) {
markCapability(capPlatform, false);
markCapability(capCrossPlatform, false);
markCapability(capConditional, false);
enableMethodSelection();
return;
}
// Check platform authenticator (biometrics)
try {
capabilities.platform = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
markCapability(capPlatform, capabilities.platform);
} catch (e) {
markCapability(capPlatform, false);
}
// Cross-platform (security keys) - generally available if WebAuthn exists
capabilities.crossPlatform = true;
markCapability(capCrossPlatform, true);
// Check conditional UI (passkey autofill)
try {
if (PublicKeyCredential.isConditionalMediationAvailable) {
capabilities.conditional = await PublicKeyCredential.isConditionalMediationAvailable();
}
markCapability(capConditional, capabilities.conditional);
} catch (e) {
markCapability(capConditional, false);
}
enableMethodSelection();
}
function markCapability(element, supported) {
const spinner = element.querySelector('wa-spinner');
if (spinner) {
const icon = document.createElement('wa-icon');
icon.setAttribute('name', supported ? 'circle-check' : 'circle-xmark');
icon.style.color = supported ? 'var(--wa-color-success)' : 'var(--wa-color-danger)';
spinner.replaceWith(icon);
}
element.classList.add(supported ? 'supported' : 'unsupported');
}
function enableMethodSelection() {
const radioPasskey = document.getElementById('radio-passkey');
const radioSecurityKey = document.getElementById('radio-security-key');
const radioQr = document.getElementById('radio-qr');
const btnNext = document.getElementById('btn-next-1');
const methodGroup = document.getElementById('auth-method-group');
// Enable options based on capabilities
if (capabilities.platform) {
radioPasskey.removeAttribute('disabled');
}
if (capabilities.crossPlatform) {
radioSecurityKey.removeAttribute('disabled');
}
// QR is always available as fallback
// Auto-select best available option
if (capabilities.platform) {
methodGroup.value = 'passkey';
selectedMethod = 'passkey';
} else if (capabilities.crossPlatform) {
methodGroup.value = 'security-key';
selectedMethod = 'security-key';
} else {
methodGroup.value = 'qr-code';
selectedMethod = 'qr-code';
}
btnNext.removeAttribute('disabled');
// Listen for method changes
methodGroup.addEventListener('wa-change', (e) => {
selectedMethod = e.target.value;
});
}
function goToStep(step) {
// Hide all steps
document.querySelectorAll('.step').forEach(el => el.classList.remove('active'));
// Update progress dots
document.querySelectorAll('.progress-dot').forEach(dot => {
const dotStep = parseInt(dot.dataset.step);
dot.classList.remove('active', 'completed');
if (dotStep < step) {
dot.classList.add('completed');
} else if (dotStep === step) {
dot.classList.add('active');
}
});
// Show appropriate step
if (step === 2 && selectedMethod) {
const methodStep = document.querySelector(`.step[data-step="2"][data-method="${selectedMethod}"]`);
if (methodStep) {
methodStep.classList.add('active');
}
} else {
const targetStep = document.querySelector(`.step[data-step="${step}"]:not([data-method])`);
if (targetStep) {
targetStep.classList.add('active');
}
}
currentStep = step;
}
// Registration functions (simulated)
async function registerPasskey() {
try {
// In production, get challenge from server
const challenge = new Uint8Array(32);
crypto.getRandomValues(challenge);
const createOptions = {
publicKey: {
challenge: challenge,
rp: {
name: "Sonr",
id: window.location.hostname
},
user: {
id: new Uint8Array(16),
name: document.querySelector('[name="username"]').value || "user@example.com",
displayName: document.querySelector('[name="display-name"]').value || "User"
},
pubKeyCredParams: [
{ type: "public-key", alg: -7 }, // ES256
{ type: "public-key", alg: -257 } // RS256
],
authenticatorSelection: {
authenticatorAttachment: "platform",
userVerification: "required",
residentKey: "required"
},
timeout: 60000
}
};
const credential = await navigator.credentials.create(createOptions);
console.log("Passkey created:", credential);
goToStep(3);
} catch (error) {
console.error("Passkey registration failed:", error);
alert("Registration failed. Please try again.");
}
}
async function registerSecurityKey() {
try {
const challenge = new Uint8Array(32);
crypto.getRandomValues(challenge);
const createOptions = {
publicKey: {
challenge: challenge,
rp: {
name: "Sonr",
id: window.location.hostname
},
user: {
id: new Uint8Array(16),
name: document.querySelector('.step[data-method="security-key"] [name="username"]').value || "user@example.com",
displayName: "User"
},
pubKeyCredParams: [
{ type: "public-key", alg: -7 },
{ type: "public-key", alg: -257 }
],
authenticatorSelection: {
authenticatorAttachment: "cross-platform",
userVerification: "preferred"
},
timeout: 60000
}
};
const credential = await navigator.credentials.create(createOptions);
console.log("Security key registered:", credential);
goToStep(3);
} catch (error) {
console.error("Security key registration failed:", error);
alert("Registration failed. Please try again.");
}
}
function verifyCode() {
const code = document.getElementById('verification-code').value;
if (code.length === 6) {
// In production, verify with server
console.log("Verifying code:", code);
goToStep(3);
} else {
alert("Please enter a valid 6-digit code");
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
// Check capabilities after a short delay to show loading state
setTimeout(checkCapabilities, 1000);
// Next button handler
document.getElementById('btn-next-1').addEventListener('click', () => {
if (selectedMethod) {
goToStep(2);
}
});
// Finish button handler
const confirmCheckbox = document.getElementById('confirm-saved');
const finishBtn = document.getElementById('btn-finish');
confirmCheckbox.addEventListener('wa-change', (e) => {
if (e.target.checked) {
finishBtn.removeAttribute('disabled');
} else {
finishBtn.setAttribute('disabled', '');
}
});
finishBtn.addEventListener('click', () => {
window.location.href = 'dashboard.html';
});
});
</script>
</body>
</html>

View File

@@ -1,536 +0,0 @@
<!DOCTYPE html>
<!--
================================================================================
TEMPL MIGRATION GUIDE: welcome.html → views/welcome.templ
================================================================================
PAGE OVERVIEW:
- Onboarding landing page with 3-step stepper (Welcome → Learn → Get Started)
- Routes users to register.html or login.html
- Network status indicator in footer
- VIEWPORT: Optimized for popup/webview (360×540 to 480×680)
VIEWPORT CONSTRAINTS (Auth Popup/Webview):
- Target sizes: 360×540 (small), 420×600 (medium), 480×680 (large)
- Card max-width: 48ch (~384px) fits all viewport sizes
- Padding scales down on smaller viewports via media queries
- Touch targets minimum 44×44px for mobile accessibility
- Footer should remain visible without scrolling on step 1/3
- Scrollable content area for step 2 (feature list)
MAIN TEMPL COMPONENT:
templ WelcomePage() {
@layouts.CenteredCard("Welcome - Sonr Motr Wallet") {
@OnboardingStepper(1)
@WelcomeStep1()
}
}
HTMX INTEGRATION:
- Replace onclick="goToStep(N)" with hx-get="/welcome/step/N" hx-target="#step-content"
- Stepper state managed server-side via URL params or session
- Navigation buttons trigger HTMX partial updates
STATE MANAGEMENT:
- Current step passed as prop: currentStep int
- Step completion status tracked server-side
================================================================================
-->
<html lang="en" class="wa-cloak">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Welcome - Sonr Motr Wallet</title>
<script src="https://cdn.sonr.org/wa/autoloader.js"></script>
<style>
:root {
--wa-color-primary: #17c2ff;
}
html, body {
min-height: 100%;
padding: 0;
margin: 0;
}
.main-centered {
display: flex;
justify-content: center;
align-items: center;
min-height: 100%;
padding: var(--wa-space-l);
box-sizing: border-box;
}
.main-centered wa-card {
width: 100%;
max-width: 48ch;
}
.step {
display: none;
}
.step.active {
display: block;
}
.onboarding-stepper {
display: flex;
justify-content: center;
align-items: center;
gap: var(--wa-space-s);
padding: var(--wa-space-m) 0;
}
.stepper-item {
display: flex;
align-items: center;
gap: var(--wa-space-xs);
}
.stepper-number {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--wa-font-size-s);
font-weight: 600;
background: var(--wa-color-neutral-200);
color: var(--wa-color-neutral-600);
transition: all 0.3s;
}
.stepper-item.active .stepper-number {
background: var(--wa-color-primary);
color: white;
box-shadow: 0 0 0 4px var(--wa-color-primary-subtle);
}
.stepper-item.completed .stepper-number {
background: var(--wa-color-success);
color: white;
}
.stepper-label {
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-500);
display: none;
}
@media (min-width: 480px) {
.stepper-label {
display: block;
}
}
/* Viewport constraints for popup/webview (360-480px width) */
@media (max-width: 400px) {
.main-centered {
padding: var(--wa-space-m);
}
.main-centered wa-card {
max-width: 100%;
}
.hero-icon {
width: 64px;
height: 64px;
}
.hero-icon wa-icon {
font-size: 32px;
}
.feature-item {
padding: var(--wa-space-s);
}
.feature-icon {
width: 32px;
height: 32px;
}
.action-card {
padding: var(--wa-space-m);
}
.action-card wa-icon {
font-size: 2rem;
}
}
@media (max-height: 600px) {
.hero-icon {
width: 56px;
height: 56px;
margin-bottom: var(--wa-space-m);
}
.hero-icon wa-icon {
font-size: 28px;
}
.onboarding-stepper {
padding: var(--wa-space-s) 0;
}
}
.stepper-item.active .stepper-label {
color: var(--wa-color-primary);
font-weight: 500;
}
.stepper-line {
width: 40px;
height: 2px;
background: var(--wa-color-neutral-200);
transition: background 0.3s;
}
.stepper-line.completed {
background: var(--wa-color-success);
}
.hero-icon {
width: 80px;
height: 80px;
border-radius: var(--wa-radius-l);
background: linear-gradient(135deg, var(--wa-color-primary), #0090ff);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto var(--wa-space-l);
box-shadow: 0 8px 32px rgba(23, 194, 255, 0.3);
}
.hero-icon wa-icon {
font-size: 40px;
color: white;
}
.feature-item {
display: flex;
align-items: flex-start;
gap: var(--wa-space-m);
padding: var(--wa-space-m);
background: var(--wa-color-surface-alt);
border-radius: var(--wa-radius-m);
}
.feature-icon {
width: 40px;
height: 40px;
border-radius: var(--wa-radius-s);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.feature-icon.security {
background: var(--wa-color-success-subtle);
color: var(--wa-color-success);
}
.feature-icon.speed {
background: var(--wa-color-primary-subtle);
color: var(--wa-color-primary);
}
.feature-icon.privacy {
background: var(--wa-color-warning-subtle);
color: var(--wa-color-warning);
}
.action-card {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--wa-space-m);
padding: var(--wa-space-xl);
background: var(--wa-color-surface-alt);
border-radius: var(--wa-radius-l);
border: 2px solid transparent;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.action-card:hover {
border-color: var(--wa-color-primary);
background: var(--wa-color-primary-subtle);
}
.action-card wa-icon {
font-size: 2.5rem;
color: var(--wa-color-primary);
}
.network-status {
display: flex;
align-items: center;
justify-content: center;
gap: var(--wa-space-xs);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--wa-color-success);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
</head>
<body>
<wa-page>
<main class="main-centered">
<wa-card>
<div slot="header">
<div class="onboarding-stepper">
<div class="stepper-item active" data-step="1">
<div class="stepper-number">1</div>
<span class="stepper-label">Welcome</span>
</div>
<div class="stepper-line"></div>
<div class="stepper-item" data-step="2">
<div class="stepper-number">2</div>
<span class="stepper-label">Learn</span>
</div>
<div class="stepper-line"></div>
<div class="stepper-item" data-step="3">
<div class="stepper-number">3</div>
<span class="stepper-label">Get Started</span>
</div>
</div>
</div>
<div class="step active" data-step="1">
<div class="wa-stack wa-gap-l">
<div class="hero-icon">
<wa-icon name="wallet" family="duotone"></wa-icon>
</div>
<div class="wa-stack wa-gap-xs" style="text-align: center;">
<h1 class="wa-heading-xl">Welcome to Sonr</h1>
<p class="wa-caption-m" style="color: var(--wa-color-neutral-600);">
Your self-sovereign identity wallet powered by WebAssembly
</p>
</div>
<wa-divider></wa-divider>
<div class="wa-stack wa-gap-s">
<div class="feature-item">
<div class="feature-icon security">
<wa-icon name="shield-check"></wa-icon>
</div>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-xs">Passwordless Security</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">
Authenticate with biometrics or hardware keys - no passwords to remember or steal
</span>
</div>
</div>
<div class="feature-item">
<div class="feature-icon speed">
<wa-icon name="bolt"></wa-icon>
</div>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-xs">WASM-Powered</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">
Wallet runs entirely in your browser - fast, secure, and always available
</span>
</div>
</div>
<div class="feature-item">
<div class="feature-icon privacy">
<wa-icon name="user-shield"></wa-icon>
</div>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-xs">You Own Your Data</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">
Self-sovereign identity means your keys never leave your device
</span>
</div>
</div>
</div>
<wa-button variant="brand" size="large" style="width: 100%;" onclick="goToStep(2)">
Learn More
<wa-icon slot="end" name="arrow-right"></wa-icon>
</wa-button>
</div>
</div>
<div class="step" data-step="2">
<div class="wa-stack wa-gap-l">
<div class="wa-stack wa-gap-xs" style="text-align: center;">
<h2 class="wa-heading-l">How Motr Works</h2>
<p class="wa-caption-m" style="color: var(--wa-color-neutral-600);">
A next-generation wallet built on WebAuthn and WASM
</p>
</div>
<wa-divider></wa-divider>
<div class="wa-stack wa-gap-m">
<wa-callout variant="neutral">
<wa-icon slot="icon" name="microchip"></wa-icon>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-xs">WebAssembly Runtime</span>
<span class="wa-caption-s">Your wallet logic runs as compiled Go code in a secure WASM sandbox, directly in your browser.</span>
</div>
</wa-callout>
<wa-callout variant="neutral">
<wa-icon slot="icon" name="fingerprint"></wa-icon>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-xs">Passkey Authentication</span>
<span class="wa-caption-s">Use Face ID, Touch ID, or hardware security keys. Your biometrics stay on your device.</span>
</div>
</wa-callout>
<wa-callout variant="neutral">
<wa-icon slot="icon" name="network-wired"></wa-icon>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-xs">Decentralized Identity</span>
<span class="wa-caption-s">Connect to any app with OpenID Connect - you control what data to share.</span>
</div>
</wa-callout>
<wa-callout variant="neutral">
<wa-icon slot="icon" name="key"></wa-icon>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-xs">Multi-Chain Support</span>
<span class="wa-caption-s">One wallet for Sonr, Ethereum, Cosmos, Bitcoin and more via IBC.</span>
</div>
</wa-callout>
</div>
<div class="wa-cluster wa-gap-s" style="justify-content: space-between;">
<wa-button variant="neutral" appearance="outlined" onclick="goToStep(1)">
<wa-icon slot="start" name="arrow-left"></wa-icon>
Back
</wa-button>
<wa-button variant="brand" onclick="goToStep(3)">
Get Started
<wa-icon slot="end" name="arrow-right"></wa-icon>
</wa-button>
</div>
</div>
</div>
<div class="step" data-step="3">
<div class="wa-stack wa-gap-l">
<div class="wa-stack wa-gap-xs" style="text-align: center;">
<h2 class="wa-heading-l">Ready to Begin?</h2>
<p class="wa-caption-m" style="color: var(--wa-color-neutral-600);">
Create a new wallet or sign in to an existing one
</p>
</div>
<wa-divider></wa-divider>
<div class="wa-grid" style="--min-column-size: 180px; gap: var(--wa-space-m);">
<div class="action-card" onclick="window.location.href='register.html'">
<wa-icon name="user-plus" family="duotone"></wa-icon>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-m">Create Wallet</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">
New to Sonr? Set up your wallet in minutes
</span>
</div>
</div>
<div class="action-card" onclick="window.location.href='login.html'">
<wa-icon name="right-to-bracket" family="duotone"></wa-icon>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-m">Sign In</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">
Access your existing wallet
</span>
</div>
</div>
</div>
<wa-divider>or</wa-divider>
<wa-button variant="neutral" appearance="outlined" style="width: 100%;" onclick="scanQR()">
<wa-icon slot="start" name="qrcode"></wa-icon>
Scan QR Code
</wa-button>
<wa-callout variant="brand" appearance="filled">
<wa-icon slot="icon" name="circle-info"></wa-icon>
<span class="wa-caption-s">
Already have a passkey registered on another device? Use QR code to sync your wallet.
</span>
</wa-callout>
<wa-button variant="neutral" appearance="plain" onclick="goToStep(2)" style="width: 100%;">
<wa-icon slot="start" name="arrow-left"></wa-icon>
Back to How It Works
</wa-button>
</div>
</div>
<footer slot="footer">
<div class="wa-stack wa-gap-m wa-align-items-center">
<div class="network-status">
<div class="status-dot"></div>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-600);">
Sonr Network: <strong style="color: var(--wa-color-success);">Operational</strong>
</span>
</div>
<div class="wa-cluster wa-justify-content-center wa-gap-m">
<wa-button appearance="plain" size="small" onclick="window.open('https://sonr.io', '_blank')">
Learn about Sonr
</wa-button>
<span style="color: var(--wa-color-neutral-300);">|</span>
<wa-button appearance="plain" size="small" onclick="window.open('https://docs.sonr.io', '_blank')">
Documentation
</wa-button>
</div>
</div>
</footer>
</wa-card>
</main>
</wa-page>
<script>
let currentStep = 1;
function goToStep(step) {
document.querySelectorAll('.step').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.stepper-item').forEach(item => {
const itemStep = parseInt(item.dataset.step);
item.classList.remove('active', 'completed');
if (itemStep < step) {
item.classList.add('completed');
} else if (itemStep === step) {
item.classList.add('active');
}
});
document.querySelectorAll('.stepper-line').forEach((line, index) => {
line.classList.toggle('completed', index < step - 1);
});
const targetStep = document.querySelector(`.step[data-step="${step}"]`);
if (targetStep) {
targetStep.classList.add('active');
}
currentStep = step;
}
function scanQR() {
alert('QR Scanner would open here - scan a code from another device to sync your wallet.');
}
</script>
</body>
</html>

View File

@@ -16,6 +16,10 @@ func RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /register/step/{step}", handleRegisterStep) mux.HandleFunc("GET /register/step/{step}", handleRegisterStep)
mux.HandleFunc("GET /register/capabilities", handleRegisterCapabilities) mux.HandleFunc("GET /register/capabilities", handleRegisterCapabilities)
mux.HandleFunc("POST /register/verify-code", handleRegisterVerifyCode) mux.HandleFunc("POST /register/verify-code", handleRegisterVerifyCode)
mux.HandleFunc("GET /login", handleLogin)
mux.HandleFunc("GET /login/step/{step}", handleLoginStep)
mux.HandleFunc("GET /login/qr-status", handleLoginQRStatus)
} }
// handleWelcome renders the full welcome page at step 1 // handleWelcome renders the full welcome page at step 1
@@ -85,3 +89,36 @@ func handleRegisterVerifyCode(w http.ResponseWriter, r *http.Request) {
} }
views.RegisterPage(state).Render(r.Context(), w) views.RegisterPage(state).Render(r.Context(), w)
} }
func handleLogin(w http.ResponseWriter, r *http.Request) {
state := views.LoginState{Step: "1"}
views.LoginPage(state).Render(r.Context(), w)
}
func handleLoginStep(w http.ResponseWriter, r *http.Request) {
step := r.PathValue("step")
if step == "" {
step = "1"
}
state := views.LoginState{Step: step}
if r.Header.Get("HX-Request") == "true" {
views.LoginStepWithOOB(state).Render(r.Context(), w)
return
}
views.LoginPage(state).Render(r.Context(), w)
}
var qrPollCount = 0
func handleLoginQRStatus(w http.ResponseWriter, r *http.Request) {
qrPollCount++
if qrPollCount >= 3 {
qrPollCount = 0
views.QRStatusSuccess().Render(r.Context(), w)
return
}
views.QRStatusWaiting().Render(r.Context(), w)
}