681 lines
23 KiB
HTML
681 lines
23 KiB
HTML
<!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>
|