feat(views): add templated login page

This commit is contained in:
2026-01-05 13:44:03 -05:00
parent 70302c27ea
commit e0ee87565d
2 changed files with 1135 additions and 0 deletions

512
views/login.templ Normal file
View File

@@ -0,0 +1,512 @@
package views
import "nebula/layouts"
type LoginState struct {
Step string
Method string
Error string
}
templ LoginPage(state LoginState) {
@layouts.CenteredCard("Sign In - Sonr") {
<div id="step-content" class="step-content">
@LoginStepContent(state)
</div>
<div id="htmx-indicator" class="htmx-indicator">
<wa-spinner></wa-spinner>
</div>
<footer slot="footer">
@LoginFooter()
</footer>
}
}
templ LoginStepContent(state LoginState) {
switch state.Step {
case "qr":
@LoginQRStep()
case "recovery":
@LoginRecoveryStep()
case "success":
@LoginSuccessStep()
default:
@LoginStep1()
}
}
templ LoginStepWithOOB(state LoginState) {
@LoginStepContent(state)
}
templ LoginStep1() {
@loginStyles()
<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-m" style="color: var(--wa-color-neutral-600);">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">
@AuthMethodCard("passkey", "fingerprint", "Face or Fingerprint", "Use your face, fingerprint, or device PIN", false)
@AuthMethodCard("security-key", "key", "Hardware Key", "Use a physical security key", false)
@AuthMethodCard("qr-code", "qrcode", "Use Another Device", "Scan a QR code with your phone", false)
</div>
<wa-divider></wa-divider>
<div class="wa-cluster wa-justify-content-between wa-gap-s">
<wa-button
appearance="plain"
size="small"
hx-get="/login/step/recovery"
hx-target="#step-content"
hx-swap="innerHTML transition:true"
hx-indicator="#htmx-indicator"
>
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>
@loginStep1Scripts()
}
templ AuthMethodCard(method string, icon string, title string, description string, disabled bool) {
<div
class={ "auth-method-card", templ.KV("disabled", disabled) }
data-method={ method }
id={ "method-" + method }
>
<wa-icon family="duotone" name={ icon }></wa-icon>
<div class="auth-method-info">
<h4 class="wa-label-m">{ title }</h4>
<p class="wa-caption-s">{ description }</p>
</div>
if method != "qr-code" {
<wa-spinner style="--size: 1.25rem; display: none;" id={ method + "-spinner" }></wa-spinner>
}
</div>
}
templ LoginQRStep() {
@loginStyles()
<div class="wa-stack wa-gap-l wa-align-items-center">
<div class="wa-stack wa-gap-xs" style="text-align: center;">
<h2 class="wa-heading-l">Use Your Phone</h2>
<p class="wa-caption-m" style="color: var(--wa-color-neutral-600);">Scan this code with your phone camera</p>
</div>
<wa-qr-code
value="https://sonr.id/auth?session=xyz789abc123"
label="Scan with your phone camera"
></wa-qr-code>
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500);">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%;"
id="qr-status"
hx-get="/login/qr-status"
hx-trigger="every 2s"
hx-target="this"
hx-swap="innerHTML"
>
<wa-spinner></wa-spinner>
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Waiting for your phone...</p>
</div>
<wa-button
appearance="outlined"
variant="neutral"
style="width: 100%;"
hx-get="/login/step/1"
hx-target="#step-content"
hx-swap="innerHTML transition:true"
hx-indicator="#htmx-indicator"
>
Back
</wa-button>
</div>
}
templ LoginRecoveryStep() {
@loginStyles()
<div class="wa-stack wa-gap-l">
<div class="wa-stack wa-gap-xs">
<h2 class="wa-heading-l">Recover Your Account</h2>
</div>
<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" style="color: var(--wa-color-neutral-600);">
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" style="color: var(--wa-color-neutral-600);">
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"
id="recovery-code"
name="recovery-code"
class="verification-code-input"
></wa-input>
</div>
</div>
<div class="wa-cluster wa-justify-content-end wa-gap-s">
<wa-button
appearance="outlined"
variant="neutral"
hx-get="/login/step/1"
hx-target="#step-content"
hx-swap="innerHTML transition:true"
hx-indicator="#htmx-indicator"
>
Back
</wa-button>
<wa-button variant="brand" id="btn-recover">
Recover
</wa-button>
</div>
</div>
@recoveryScripts()
}
templ LoginSuccessStep() {
@loginStyles()
<div class="wa-stack wa-gap-l wa-align-items-center">
<wa-icon name="circle-check" family="duotone" class="success-icon"></wa-icon>
<div class="wa-stack wa-gap-xs" style="text-align: center;">
<h2 class="wa-heading-l">You're In</h2>
<p class="wa-caption-m" style="color: var(--wa-color-neutral-600);">
Signed in successfully.
</p>
</div>
<wa-button variant="brand" style="width: 100%;" onclick="window.location.href='/dashboard'">
Go to Dashboard
<wa-icon slot="end" variant="regular" name="arrow-right"></wa-icon>
</wa-button>
</div>
}
templ QRStatusWaiting() {
<wa-spinner></wa-spinner>
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Waiting for your phone...</p>
}
templ QRStatusSuccess() {
<wa-icon name="circle-check" style="font-size: 2rem; color: var(--wa-color-success);"></wa-icon>
<p class="wa-caption-s" style="color: var(--wa-color-success);">Phone connected!</p>
<script>
setTimeout(() => {
htmx.ajax('GET', '/login/step/success', {target: '#step-content', swap: 'innerHTML transition:true'});
}, 500);
</script>
}
templ LoginFooter() {
<div class="wa-cluster wa-justify-content-center wa-gap-m">
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Don't have an account?</span>
<wa-button appearance="plain" size="small" onclick="window.location.href='/register'">
Create Account
</wa-button>
</div>
}
templ loginStyles() {
<style>
.htmx-indicator {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 100;
}
.htmx-request .htmx-indicator {
display: flex;
}
.htmx-request.step-content {
opacity: 0.5;
pointer-events: none;
transition: opacity 0.2s ease;
}
@view-transition {
navigation: auto;
}
::view-transition-old(step-content),
::view-transition-new(step-content) {
animation-duration: 0.25s;
}
.step-content {
view-transition-name: step-content;
position: relative;
}
.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:not(.disabled) {
background: var(--wa-color-surface-alt-hover, var(--wa-color-neutral-100));
}
.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);
}
.success-icon {
font-size: 4rem;
color: var(--wa-color-success);
}
.verification-code-input {
text-align: center;
font-size: 1.5rem;
letter-spacing: 0.3em;
max-width: 180px;
}
@media (max-width: 400px) {
.auth-method-card {
padding: var(--wa-space-s);
gap: var(--wa-space-s);
}
.auth-method-card wa-icon {
font-size: 1.5rem;
}
.verification-code-input {
font-size: 1.25rem;
max-width: 160px;
}
.success-icon {
font-size: 3rem;
}
}
@media (max-height: 600px) {
.auth-method-card {
padding: var(--wa-space-s);
}
}
</style>
}
templ loginStep1Scripts() {
<script>
(function() {
let selectedMethod = null;
const btnSignin = document.getElementById('btn-signin');
async function checkCapabilities() {
if (!window.PublicKeyCredential) {
document.getElementById('method-passkey')?.classList.add('disabled');
document.getElementById('method-security-key')?.classList.add('disabled');
return;
}
try {
const hasPlatform = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
if (!hasPlatform) {
document.getElementById('method-passkey')?.classList.add('disabled');
}
} catch (e) {
document.getElementById('method-passkey')?.classList.add('disabled');
}
try {
if (PublicKeyCredential.isConditionalMediationAvailable) {
const hasConditional = await PublicKeyCredential.isConditionalMediationAvailable();
if (hasConditional) {
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 auth successful:", credential);
htmx.ajax('GET', '/login/step/success', {target: '#step-content', swap: 'innerHTML transition:true'});
}
} catch (error) {
console.log("Conditional UI not used:", error.message);
}
}
function selectMethod(method) {
const card = document.getElementById('method-' + method);
if (!card || card.classList.contains('disabled')) return;
document.querySelectorAll('.auth-method-card').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
selectedMethod = method;
btnSignin?.removeAttribute('disabled');
}
async function signIn() {
if (!selectedMethod) return;
if (selectedMethod === 'qr-code') {
htmx.ajax('GET', '/login/step/qr', {target: '#step-content', swap: 'innerHTML transition:true'});
return;
}
const spinner = document.getElementById(selectedMethod + '-spinner');
if (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("Auth successful:", credential);
htmx.ajax('GET', '/login/step/success', {target: '#step-content', swap: 'innerHTML transition:true'});
} catch (error) {
console.error("Auth failed:", error);
alert("Authentication failed: " + error.message);
} finally {
if (spinner) spinner.style.display = 'none';
}
}
document.querySelectorAll('.auth-method-card').forEach(card => {
card.addEventListener('click', () => selectMethod(card.dataset.method));
});
btnSignin?.addEventListener('click', signIn);
checkCapabilities();
})();
</script>
}
templ recoveryScripts() {
<script>
(function() {
const methodGroup = document.getElementById('recovery-method-group');
const keyForm = document.getElementById('recovery-key-form');
const qrForm = document.getElementById('recovery-qr-form');
const btnRecover = document.getElementById('btn-recover');
methodGroup?.addEventListener('wa-change', (e) => {
if (e.target.value === 'recovery-key') {
keyForm.style.display = 'block';
qrForm.style.display = 'none';
} else {
keyForm.style.display = 'none';
qrForm.style.display = 'block';
}
});
btnRecover?.addEventListener('click', () => {
const method = methodGroup?.value;
if (method === 'recovery-key') {
const key = document.getElementById('recovery-key-input')?.value;
if (key?.startsWith('rk_')) {
console.log("Recovery key validated");
htmx.ajax('GET', '/login/step/success', {target: '#step-content', swap: 'innerHTML transition:true'});
} else {
alert("Please enter a valid recovery key (starts with rk_)");
}
} else {
const code = document.getElementById('recovery-code')?.value;
if (code?.length === 6) {
console.log("Recovery code verified");
htmx.ajax('GET', '/login/step/success', {target: '#step-content', swap: 'innerHTML transition:true'});
} else {
alert("Please enter a valid 6-digit code");
}
}
});
})();
</script>
}

623
views/login_templ.go Normal file
View File

@@ -0,0 +1,623 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.977
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "nebula/layouts"
type LoginState struct {
Step string
Method string
Error string
}
func LoginPage(state LoginState) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"step-content\" class=\"step-content\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = LoginStepContent(state).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div><div id=\"htmx-indicator\" class=\"htmx-indicator\"><wa-spinner></wa-spinner></div><footer slot=\"footer\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = LoginFooter().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = layouts.CenteredCard("Sign In - Sonr").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func LoginStepContent(state LoginState) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
switch state.Step {
case "qr":
templ_7745c5c3_Err = LoginQRStep().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "recovery":
templ_7745c5c3_Err = LoginRecoveryStep().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "success":
templ_7745c5c3_Err = LoginSuccessStep().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
default:
templ_7745c5c3_Err = LoginStep1().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
func LoginStepWithOOB(state LoginState) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = LoginStepContent(state).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func LoginStep1() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = loginStyles().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<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-m\" style=\"color: var(--wa-color-neutral-600);\">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\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = AuthMethodCard("passkey", "fingerprint", "Face or Fingerprint", "Use your face, fingerprint, or device PIN", false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = AuthMethodCard("security-key", "key", "Hardware Key", "Use a physical security key", false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = AuthMethodCard("qr-code", "qrcode", "Use Another Device", "Scan a QR code with your phone", false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><wa-divider></wa-divider><div class=\"wa-cluster wa-justify-content-between wa-gap-s\"><wa-button appearance=\"plain\" size=\"small\" hx-get=\"/login/step/recovery\" hx-target=\"#step-content\" hx-swap=\"innerHTML transition:true\" hx-indicator=\"#htmx-indicator\">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>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = loginStep1Scripts().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func AuthMethodCard(method string, icon string, title string, description string, disabled bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
if templ_7745c5c3_Var6 == nil {
templ_7745c5c3_Var6 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var7 = []any{"auth-method-card", templ.KV("disabled", disabled)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var7...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var7).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/login.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" data-method=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/login.templ`, Line: 88, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs("method-" + method)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/login.templ`, Line: 89, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"><wa-icon family=\"duotone\" name=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(icon)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/login.templ`, Line: 91, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"></wa-icon><div class=\"auth-method-info\"><h4 class=\"wa-label-m\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/login.templ`, Line: 93, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</h4><p class=\"wa-caption-s\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/login.templ`, Line: 94, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if method != "qr-code" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<wa-spinner style=\"--size: 1.25rem; display: none;\" id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(method + "-spinner")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/login.templ`, Line: 97, Col: 79}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"></wa-spinner>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func LoginQRStep() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var15 := templ.GetChildren(ctx)
if templ_7745c5c3_Var15 == nil {
templ_7745c5c3_Var15 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = loginStyles().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"wa-stack wa-gap-l wa-align-items-center\"><div class=\"wa-stack wa-gap-xs\" style=\"text-align: center;\"><h2 class=\"wa-heading-l\">Use Your Phone</h2><p class=\"wa-caption-m\" style=\"color: var(--wa-color-neutral-600);\">Scan this code with your phone camera</p></div><wa-qr-code value=\"https://sonr.id/auth?session=xyz789abc123\" label=\"Scan with your phone camera\"></wa-qr-code><p class=\"wa-caption-s\" style=\"color: var(--wa-color-neutral-500);\">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%;\" id=\"qr-status\" hx-get=\"/login/qr-status\" hx-trigger=\"every 2s\" hx-target=\"this\" hx-swap=\"innerHTML\"><wa-spinner></wa-spinner><p class=\"wa-caption-s\" style=\"color: var(--wa-color-neutral-500);\">Waiting for your phone...</p></div><wa-button appearance=\"outlined\" variant=\"neutral\" style=\"width: 100%;\" hx-get=\"/login/step/1\" hx-target=\"#step-content\" hx-swap=\"innerHTML transition:true\" hx-indicator=\"#htmx-indicator\">Back</wa-button></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func LoginRecoveryStep() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var16 := templ.GetChildren(ctx)
if templ_7745c5c3_Var16 == nil {
templ_7745c5c3_Var16 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = loginStyles().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"wa-stack wa-gap-l\"><div class=\"wa-stack wa-gap-xs\"><h2 class=\"wa-heading-l\">Recover Your Account</h2></div><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\" style=\"color: var(--wa-color-neutral-600);\">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\" style=\"color: var(--wa-color-neutral-600);\">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\" id=\"recovery-code\" name=\"recovery-code\" class=\"verification-code-input\"></wa-input></div></div><div class=\"wa-cluster wa-justify-content-end wa-gap-s\"><wa-button appearance=\"outlined\" variant=\"neutral\" hx-get=\"/login/step/1\" hx-target=\"#step-content\" hx-swap=\"innerHTML transition:true\" hx-indicator=\"#htmx-indicator\">Back</wa-button> <wa-button variant=\"brand\" id=\"btn-recover\">Recover</wa-button></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = recoveryScripts().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func LoginSuccessStep() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var17 := templ.GetChildren(ctx)
if templ_7745c5c3_Var17 == nil {
templ_7745c5c3_Var17 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = loginStyles().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"wa-stack wa-gap-l wa-align-items-center\"><wa-icon name=\"circle-check\" family=\"duotone\" class=\"success-icon\"></wa-icon><div class=\"wa-stack wa-gap-xs\" style=\"text-align: center;\"><h2 class=\"wa-heading-l\">You're In</h2><p class=\"wa-caption-m\" style=\"color: var(--wa-color-neutral-600);\">Signed in successfully.</p></div><wa-button variant=\"brand\" style=\"width: 100%;\" onclick=\"window.location.href='/dashboard'\">Go to Dashboard <wa-icon slot=\"end\" variant=\"regular\" name=\"arrow-right\"></wa-icon></wa-button></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func QRStatusWaiting() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var18 := templ.GetChildren(ctx)
if templ_7745c5c3_Var18 == nil {
templ_7745c5c3_Var18 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<wa-spinner></wa-spinner><p class=\"wa-caption-s\" style=\"color: var(--wa-color-neutral-500);\">Waiting for your phone...</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func QRStatusSuccess() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var19 := templ.GetChildren(ctx)
if templ_7745c5c3_Var19 == nil {
templ_7745c5c3_Var19 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<wa-icon name=\"circle-check\" style=\"font-size: 2rem; color: var(--wa-color-success);\"></wa-icon><p class=\"wa-caption-s\" style=\"color: var(--wa-color-success);\">Phone connected!</p><script>\n\t\tsetTimeout(() => {\n\t\t\thtmx.ajax('GET', '/login/step/success', {target: '#step-content', swap: 'innerHTML transition:true'});\n\t\t}, 500);\n\t</script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func LoginFooter() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var20 := templ.GetChildren(ctx)
if templ_7745c5c3_Var20 == nil {
templ_7745c5c3_Var20 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<div class=\"wa-cluster wa-justify-content-center wa-gap-m\"><span class=\"wa-caption-s\" style=\"color: var(--wa-color-neutral-500);\">Don't have an account?</span> <wa-button appearance=\"plain\" size=\"small\" onclick=\"window.location.href='/register'\">Create Account</wa-button></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func loginStyles() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var21 := templ.GetChildren(ctx)
if templ_7745c5c3_Var21 == nil {
templ_7745c5c3_Var21 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<style>\n\t\t.htmx-indicator {\n\t\t\tdisplay: none;\n\t\t\tposition: absolute;\n\t\t\ttop: 50%;\n\t\t\tleft: 50%;\n\t\t\ttransform: translate(-50%, -50%);\n\t\t\tz-index: 100;\n\t\t}\n\t\t.htmx-request .htmx-indicator {\n\t\t\tdisplay: flex;\n\t\t}\n\t\t.htmx-request.step-content {\n\t\t\topacity: 0.5;\n\t\t\tpointer-events: none;\n\t\t\ttransition: opacity 0.2s ease;\n\t\t}\n\t\t@view-transition {\n\t\t\tnavigation: auto;\n\t\t}\n\t\t::view-transition-old(step-content),\n\t\t::view-transition-new(step-content) {\n\t\t\tanimation-duration: 0.25s;\n\t\t}\n\t\t.step-content {\n\t\t\tview-transition-name: step-content;\n\t\t\tposition: relative;\n\t\t}\n\t\t.auth-method-card {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tgap: var(--wa-space-m);\n\t\t\tpadding: var(--wa-space-m);\n\t\t\tborder-radius: var(--wa-radius-m);\n\t\t\tbackground: var(--wa-color-surface-alt);\n\t\t\tcursor: pointer;\n\t\t\tborder: 2px solid transparent;\n\t\t\ttransition: border-color 0.2s, background 0.2s;\n\t\t}\n\t\t.auth-method-card:hover:not(.disabled) {\n\t\t\tbackground: var(--wa-color-surface-alt-hover, var(--wa-color-neutral-100));\n\t\t}\n\t\t.auth-method-card.selected {\n\t\t\tborder-color: var(--wa-color-primary);\n\t\t}\n\t\t.auth-method-card.disabled {\n\t\t\topacity: 0.5;\n\t\t\tcursor: not-allowed;\n\t\t}\n\t\t.auth-method-card wa-icon {\n\t\t\tfont-size: 2rem;\n\t\t\tcolor: var(--wa-color-primary);\n\t\t}\n\t\t.auth-method-info {\n\t\t\tflex: 1;\n\t\t}\n\t\t.auth-method-info h4 {\n\t\t\tmargin: 0 0 var(--wa-space-2xs) 0;\n\t\t}\n\t\t.auth-method-info p {\n\t\t\tmargin: 0;\n\t\t\tcolor: var(--wa-color-neutral-600);\n\t\t}\n\t\t.success-icon {\n\t\t\tfont-size: 4rem;\n\t\t\tcolor: var(--wa-color-success);\n\t\t}\n\t\t.verification-code-input {\n\t\t\ttext-align: center;\n\t\t\tfont-size: 1.5rem;\n\t\t\tletter-spacing: 0.3em;\n\t\t\tmax-width: 180px;\n\t\t}\n\t\t@media (max-width: 400px) {\n\t\t\t.auth-method-card {\n\t\t\t\tpadding: var(--wa-space-s);\n\t\t\t\tgap: var(--wa-space-s);\n\t\t\t}\n\t\t\t.auth-method-card wa-icon {\n\t\t\t\tfont-size: 1.5rem;\n\t\t\t}\n\t\t\t.verification-code-input {\n\t\t\t\tfont-size: 1.25rem;\n\t\t\t\tmax-width: 160px;\n\t\t\t}\n\t\t\t.success-icon {\n\t\t\t\tfont-size: 3rem;\n\t\t\t}\n\t\t}\n\t\t@media (max-height: 600px) {\n\t\t\t.auth-method-card {\n\t\t\t\tpadding: var(--wa-space-s);\n\t\t\t}\n\t\t}\n\t</style>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func loginStep1Scripts() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var22 := templ.GetChildren(ctx)
if templ_7745c5c3_Var22 == nil {
templ_7745c5c3_Var22 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<script>\n\t\t(function() {\n\t\t\tlet selectedMethod = null;\n\t\t\tconst btnSignin = document.getElementById('btn-signin');\n\t\t\t\n\t\t\tasync function checkCapabilities() {\n\t\t\t\tif (!window.PublicKeyCredential) {\n\t\t\t\t\tdocument.getElementById('method-passkey')?.classList.add('disabled');\n\t\t\t\t\tdocument.getElementById('method-security-key')?.classList.add('disabled');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\ttry {\n\t\t\t\t\tconst hasPlatform = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();\n\t\t\t\t\tif (!hasPlatform) {\n\t\t\t\t\t\tdocument.getElementById('method-passkey')?.classList.add('disabled');\n\t\t\t\t\t}\n\t\t\t\t} catch (e) {\n\t\t\t\t\tdocument.getElementById('method-passkey')?.classList.add('disabled');\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\ttry {\n\t\t\t\t\tif (PublicKeyCredential.isConditionalMediationAvailable) {\n\t\t\t\t\t\tconst hasConditional = await PublicKeyCredential.isConditionalMediationAvailable();\n\t\t\t\t\t\tif (hasConditional) {\n\t\t\t\t\t\t\tstartConditionalUI();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch (e) {}\n\t\t\t}\n\t\t\t\n\t\t\tasync function startConditionalUI() {\n\t\t\t\ttry {\n\t\t\t\t\tconst challenge = new Uint8Array(32);\n\t\t\t\t\tcrypto.getRandomValues(challenge);\n\t\t\t\t\t\n\t\t\t\t\tconst credential = await navigator.credentials.get({\n\t\t\t\t\t\tpublicKey: {\n\t\t\t\t\t\t\tchallenge: challenge,\n\t\t\t\t\t\t\trpId: window.location.hostname,\n\t\t\t\t\t\t\tuserVerification: \"preferred\",\n\t\t\t\t\t\t\ttimeout: 300000\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmediation: \"conditional\"\n\t\t\t\t\t});\n\t\t\t\t\t\n\t\t\t\t\tif (credential) {\n\t\t\t\t\t\tconsole.log(\"Conditional UI auth successful:\", credential);\n\t\t\t\t\t\thtmx.ajax('GET', '/login/step/success', {target: '#step-content', swap: 'innerHTML transition:true'});\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.log(\"Conditional UI not used:\", error.message);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tfunction selectMethod(method) {\n\t\t\t\tconst card = document.getElementById('method-' + method);\n\t\t\t\tif (!card || card.classList.contains('disabled')) return;\n\t\t\t\t\n\t\t\t\tdocument.querySelectorAll('.auth-method-card').forEach(c => c.classList.remove('selected'));\n\t\t\t\tcard.classList.add('selected');\n\t\t\t\tselectedMethod = method;\n\t\t\t\tbtnSignin?.removeAttribute('disabled');\n\t\t\t}\n\t\t\t\n\t\t\tasync function signIn() {\n\t\t\t\tif (!selectedMethod) return;\n\t\t\t\t\n\t\t\t\tif (selectedMethod === 'qr-code') {\n\t\t\t\t\thtmx.ajax('GET', '/login/step/qr', {target: '#step-content', swap: 'innerHTML transition:true'});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tconst spinner = document.getElementById(selectedMethod + '-spinner');\n\t\t\t\tif (spinner) spinner.style.display = 'block';\n\t\t\t\t\n\t\t\t\ttry {\n\t\t\t\t\tconst challenge = new Uint8Array(32);\n\t\t\t\t\tcrypto.getRandomValues(challenge);\n\t\t\t\t\t\n\t\t\t\t\tconst getOptions = {\n\t\t\t\t\t\tpublicKey: {\n\t\t\t\t\t\t\tchallenge: challenge,\n\t\t\t\t\t\t\trpId: window.location.hostname,\n\t\t\t\t\t\t\tuserVerification: selectedMethod === 'passkey' ? \"required\" : \"preferred\",\n\t\t\t\t\t\t\ttimeout: 60000\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\t\n\t\t\t\t\tif (selectedMethod === 'security-key') {\n\t\t\t\t\t\tgetOptions.publicKey.allowCredentials = [];\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tconst credential = await navigator.credentials.get(getOptions);\n\t\t\t\t\tconsole.log(\"Auth successful:\", credential);\n\t\t\t\t\thtmx.ajax('GET', '/login/step/success', {target: '#step-content', swap: 'innerHTML transition:true'});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\"Auth failed:\", error);\n\t\t\t\t\talert(\"Authentication failed: \" + error.message);\n\t\t\t\t} finally {\n\t\t\t\t\tif (spinner) spinner.style.display = 'none';\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tdocument.querySelectorAll('.auth-method-card').forEach(card => {\n\t\t\t\tcard.addEventListener('click', () => selectMethod(card.dataset.method));\n\t\t\t});\n\t\t\t\n\t\t\tbtnSignin?.addEventListener('click', signIn);\n\t\t\tcheckCapabilities();\n\t\t})();\n\t</script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func recoveryScripts() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var23 := templ.GetChildren(ctx)
if templ_7745c5c3_Var23 == nil {
templ_7745c5c3_Var23 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<script>\n\t\t(function() {\n\t\t\tconst methodGroup = document.getElementById('recovery-method-group');\n\t\t\tconst keyForm = document.getElementById('recovery-key-form');\n\t\t\tconst qrForm = document.getElementById('recovery-qr-form');\n\t\t\tconst btnRecover = document.getElementById('btn-recover');\n\t\t\t\n\t\t\tmethodGroup?.addEventListener('wa-change', (e) => {\n\t\t\t\tif (e.target.value === 'recovery-key') {\n\t\t\t\t\tkeyForm.style.display = 'block';\n\t\t\t\t\tqrForm.style.display = 'none';\n\t\t\t\t} else {\n\t\t\t\t\tkeyForm.style.display = 'none';\n\t\t\t\t\tqrForm.style.display = 'block';\n\t\t\t\t}\n\t\t\t});\n\t\t\t\n\t\t\tbtnRecover?.addEventListener('click', () => {\n\t\t\t\tconst method = methodGroup?.value;\n\t\t\t\tif (method === 'recovery-key') {\n\t\t\t\t\tconst key = document.getElementById('recovery-key-input')?.value;\n\t\t\t\t\tif (key?.startsWith('rk_')) {\n\t\t\t\t\t\tconsole.log(\"Recovery key validated\");\n\t\t\t\t\t\thtmx.ajax('GET', '/login/step/success', {target: '#step-content', swap: 'innerHTML transition:true'});\n\t\t\t\t\t} else {\n\t\t\t\t\t\talert(\"Please enter a valid recovery key (starts with rk_)\");\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tconst code = document.getElementById('recovery-code')?.value;\n\t\t\t\t\tif (code?.length === 6) {\n\t\t\t\t\t\tconsole.log(\"Recovery code verified\");\n\t\t\t\t\t\thtmx.ajax('GET', '/login/step/success', {target: '#step-content', swap: 'innerHTML transition:true'});\n\t\t\t\t\t} else {\n\t\t\t\t\t\talert(\"Please enter a valid 6-digit code\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t})();\n\t</script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate