Files
nebula/_migrate/demo.html

655 lines
23 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<!--
================================================================================
TEMPL MIGRATION GUIDE: demo.html → views/demo.templ
================================================================================
PAGE OVERVIEW:
- Demo page showcasing auth views in mini browser window webview style
- Similar to "Sign in with Google" popup windows
- Demonstrates welcome, login, register, and authorize flows in iframe
- Controls to switch between views and adjust window size
- Simulates how dApps would integrate Sonr authentication
MAIN TEMPL COMPONENT:
templ DemoPage() {
@layouts.Base("Auth Demo - Sonr Motr Wallet") {
@DemoHeader()
@DemoControls()
@WebviewFrame()
@IntegrationCodeSamples()
}
}
HTMX INTEGRATION:
- View switching: hx-get="/demo/frame/{view}" hx-target="#webview-frame"
- Size presets: Client-side JavaScript for iframe resizing
- Theme toggle: Client-side for dark/light mode preview
SUB-COMPONENTS TO EXTRACT:
- DemoHeader()
- DemoControls()
- ViewSelector(views []string, selected string)
- SizePresets(sizes []Size, selected string)
- WebviewFrame(src string, width int, height int)
- IntegrationCodeSamples()
- CodeBlock(language string, code string)
STATE/PROPS:
type DemoState struct {
CurrentView string // "welcome", "login", "register", "authorize"
FrameWidth int
FrameHeight int
Theme string // "light", "dark", "system"
}
type Size struct {
Name string
Width int
Height int
}
================================================================================
-->
<html lang="en" class="wa-cloak">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Auth Demo - 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;
}
.demo-layout {
min-height: 100vh;
background: var(--wa-color-surface-alt);
}
.demo-header {
background: var(--wa-color-surface);
border-bottom: 1px solid var(--wa-color-neutral-200);
padding: var(--wa-space-m) var(--wa-space-xl);
}
.demo-header-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.demo-main {
max-width: 1400px;
margin: 0 auto;
padding: var(--wa-space-xl);
}
.demo-controls {
background: var(--wa-color-surface);
border-radius: var(--wa-radius-l);
padding: var(--wa-space-l);
margin-bottom: var(--wa-space-xl);
}
.controls-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--wa-space-l);
align-items: end;
}
.webview-container {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--wa-space-l);
}
.webview-wrapper {
position: relative;
background: var(--wa-color-neutral-900);
border-radius: var(--wa-radius-l);
padding: var(--wa-space-xs);
box-shadow:
0 0 0 1px var(--wa-color-neutral-700),
0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.webview-titlebar {
display: flex;
align-items: center;
gap: var(--wa-space-s);
padding: var(--wa-space-xs) var(--wa-space-s);
background: var(--wa-color-neutral-800);
border-radius: var(--wa-radius-m) var(--wa-radius-m) 0 0;
}
.titlebar-buttons {
display: flex;
gap: var(--wa-space-2xs);
}
.titlebar-btn {
width: 12px;
height: 12px;
border-radius: 50%;
}
.titlebar-btn.close { background: #ff5f57; }
.titlebar-btn.minimize { background: #febc2e; }
.titlebar-btn.maximize { background: #28c840; }
.titlebar-url {
flex: 1;
background: var(--wa-color-neutral-700);
border-radius: var(--wa-radius-s);
padding: var(--wa-space-2xs) var(--wa-space-s);
font-family: var(--wa-font-mono);
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-300);
display: flex;
align-items: center;
gap: var(--wa-space-xs);
}
.titlebar-url wa-icon {
font-size: 10px;
color: var(--wa-color-success);
}
.webview-frame {
border: none;
border-radius: 0 0 var(--wa-radius-m) var(--wa-radius-m);
background: white;
transition: width 0.3s ease, height 0.3s ease;
}
.size-indicator {
font-family: var(--wa-font-mono);
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-500);
text-align: center;
}
.integration-section {
margin-top: var(--wa-space-2xl);
}
.code-block {
background: var(--wa-color-neutral-900);
border-radius: var(--wa-radius-m);
padding: var(--wa-space-m);
overflow-x: auto;
}
.code-block pre {
margin: 0;
font-family: var(--wa-font-mono);
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-100);
line-height: 1.6;
}
.code-block .comment { color: var(--wa-color-neutral-500); }
.code-block .keyword { color: #ff79c6; }
.code-block .string { color: #f1fa8c; }
.code-block .function { color: #50fa7b; }
.code-block .number { color: #bd93f9; }
.view-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--wa-space-m);
margin-top: var(--wa-space-xl);
}
.view-card {
background: var(--wa-color-surface);
border-radius: var(--wa-radius-m);
padding: var(--wa-space-m);
border: 2px solid transparent;
cursor: pointer;
transition: border-color 0.2s, transform 0.2s;
}
.view-card:hover {
border-color: var(--wa-color-primary);
transform: translateY(-2px);
}
.view-card.active {
border-color: var(--wa-color-primary);
background: var(--wa-color-primary-subtle);
}
.view-card-icon {
width: 48px;
height: 48px;
border-radius: var(--wa-radius-m);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
margin-bottom: var(--wa-space-s);
}
.view-card-icon.welcome { background: var(--wa-color-primary-subtle); color: var(--wa-color-primary); }
.view-card-icon.login { background: var(--wa-color-success-subtle); color: var(--wa-color-success); }
.view-card-icon.register { background: var(--wa-color-warning-subtle); color: var(--wa-color-warning); }
.view-card-icon.authorize { background: var(--wa-color-danger-subtle); color: var(--wa-color-danger); }
.popup-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
z-index: 1000;
justify-content: center;
align-items: center;
}
.popup-overlay.active {
display: flex;
}
.popup-window {
animation: popup-in 0.3s ease;
}
@keyframes popup-in {
from {
opacity: 0;
transform: scale(0.9) translateY(20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
</style>
</head>
<body>
<div class="demo-layout">
<header class="demo-header">
<div class="demo-header-content">
<div class="wa-cluster wa-gap-m">
<wa-icon name="wallet" family="duotone" style="font-size: 28px; color: var(--wa-color-primary);"></wa-icon>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-m">Sonr Auth Demo</span>
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Mini Browser Webview Showcase</span>
</div>
</div>
<div class="wa-cluster wa-gap-s">
<wa-button appearance="plain" size="small" onclick="window.open('https://docs.sonr.io', '_blank')">
<wa-icon slot="start" name="book"></wa-icon>
Docs
</wa-button>
<wa-button appearance="plain" size="small" onclick="window.open('https://github.com/sonr-io', '_blank')">
<wa-icon slot="start" name="github" family="brands"></wa-icon>
GitHub
</wa-button>
</div>
</div>
</header>
<main class="demo-main">
<div class="wa-stack wa-gap-m" style="text-align: center; margin-bottom: var(--wa-space-xl);">
<h1 class="wa-heading-xl">Authentication Webview Demo</h1>
<p class="wa-caption-m" style="color: var(--wa-color-neutral-600); max-width: 600px; margin: 0 auto;">
Preview how Sonr authentication flows appear in popup windows and embedded webviews,
similar to "Sign in with Google" or "Connect Wallet" experiences.
</p>
</div>
<div class="demo-controls">
<div class="controls-grid">
<div class="wa-stack wa-gap-xs">
<label class="wa-label-s">View</label>
<wa-select id="view-select" value="welcome">
<wa-option value="welcome">Welcome (Onboarding)</wa-option>
<wa-option value="login">Login (Sign In)</wa-option>
<wa-option value="register">Register (Create Account)</wa-option>
<wa-option value="authorize">Authorize (OAuth Consent)</wa-option>
</wa-select>
</div>
<div class="wa-stack wa-gap-xs">
<label class="wa-label-s">Window Size</label>
<wa-select id="size-select" value="medium">
<wa-option value="small">Small (360 × 540)</wa-option>
<wa-option value="medium">Medium (420 × 600)</wa-option>
<wa-option value="large">Large (480 × 680)</wa-option>
<wa-option value="mobile">Mobile (375 × 667)</wa-option>
</wa-select>
</div>
<div class="wa-stack wa-gap-xs">
<label class="wa-label-s">Actions</label>
<div class="wa-cluster wa-gap-s">
<wa-button variant="brand" id="popup-btn">
<wa-icon slot="start" name="arrow-up-right-from-square"></wa-icon>
Open as Popup
</wa-button>
<wa-button variant="neutral" appearance="outlined" id="new-tab-btn">
<wa-icon slot="start" name="arrow-up-right"></wa-icon>
New Tab
</wa-button>
</div>
</div>
</div>
</div>
<div class="webview-container">
<div class="webview-wrapper" id="webview-wrapper">
<div class="webview-titlebar">
<div class="titlebar-buttons">
<div class="titlebar-btn close"></div>
<div class="titlebar-btn minimize"></div>
<div class="titlebar-btn maximize"></div>
</div>
<div class="titlebar-url" id="titlebar-url">
<wa-icon name="lock"></wa-icon>
<span id="url-display">auth.sonr.io/welcome</span>
</div>
</div>
<iframe
id="webview-frame"
class="webview-frame"
src="welcome.html"
width="420"
height="600"
></iframe>
</div>
<div class="size-indicator" id="size-indicator">420 × 600</div>
</div>
<div class="view-grid">
<div class="view-card active" data-view="welcome">
<div class="view-card-icon welcome">
<wa-icon name="hand-wave"></wa-icon>
</div>
<span class="wa-heading-s">Welcome</span>
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500); margin: var(--wa-space-xs) 0 0 0;">
Onboarding flow with feature highlights and navigation to login/register
</p>
</div>
<div class="view-card" data-view="login">
<div class="view-card-icon login">
<wa-icon name="right-to-bracket"></wa-icon>
</div>
<span class="wa-heading-s">Login</span>
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500); margin: var(--wa-space-xs) 0 0 0;">
WebAuthn sign-in with passkey, security key, and QR code options
</p>
</div>
<div class="view-card" data-view="register">
<div class="view-card-icon register">
<wa-icon name="user-plus"></wa-icon>
</div>
<span class="wa-heading-s">Register</span>
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500); margin: var(--wa-space-xs) 0 0 0;">
Account creation wizard with device capability detection
</p>
</div>
<div class="view-card" data-view="authorize">
<div class="view-card-icon authorize">
<wa-icon name="shield-check"></wa-icon>
</div>
<span class="wa-heading-s">Authorize</span>
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500); margin: var(--wa-space-xs) 0 0 0;">
OAuth consent screen for connect, sign, and transaction requests
</p>
</div>
</div>
<div class="integration-section">
<wa-card>
<div slot="header">
<span class="wa-heading-m">Integration Examples</span>
</div>
<wa-tab-group>
<wa-tab panel="popup">Popup Window</wa-tab>
<wa-tab panel="iframe">Embedded Iframe</wa-tab>
<wa-tab panel="redirect">Redirect Flow</wa-tab>
<wa-tab-panel name="popup">
<div class="wa-stack wa-gap-m">
<p class="wa-caption-s" style="color: var(--wa-color-neutral-600);">
Open authentication in a centered popup window, similar to "Sign in with Google".
</p>
<div class="code-block">
<pre><span class="comment">// Open Sonr Auth as popup window</span>
<span class="keyword">function</span> <span class="function">openSonrAuth</span>() {
<span class="keyword">const</span> width = <span class="number">420</span>;
<span class="keyword">const</span> height = <span class="number">600</span>;
<span class="keyword">const</span> left = (screen.width - width) / <span class="number">2</span>;
<span class="keyword">const</span> top = (screen.height - height) / <span class="number">2</span>;
<span class="keyword">const</span> popup = window.<span class="function">open</span>(
<span class="string">'https://auth.sonr.io/authorize?client_id=YOUR_APP&scope=openid+profile'</span>,
<span class="string">'SonrAuth'</span>,
<span class="string">`width=${width},height=${height},left=${left},top=${top}`</span>
);
<span class="comment">// Listen for auth completion</span>
window.<span class="function">addEventListener</span>(<span class="string">'message'</span>, (event) => {
<span class="keyword">if</span> (event.origin === <span class="string">'https://auth.sonr.io'</span>) {
<span class="keyword">const</span> { token, user } = event.data;
<span class="function">handleAuthSuccess</span>(token, user);
popup.<span class="function">close</span>();
}
});
}</pre>
</div>
</div>
</wa-tab-panel>
<wa-tab-panel name="iframe">
<div class="wa-stack wa-gap-m">
<p class="wa-caption-s" style="color: var(--wa-color-neutral-600);">
Embed authentication inline within a modal or container.
</p>
<div class="code-block">
<pre><span class="comment">// Embed Sonr Auth in iframe</span>
<span class="keyword">const</span> container = document.<span class="function">getElementById</span>(<span class="string">'auth-container'</span>);
<span class="keyword">const</span> iframe = document.<span class="function">createElement</span>(<span class="string">'iframe'</span>);
iframe.src = <span class="string">'https://auth.sonr.io/authorize?client_id=YOUR_APP&mode=embedded'</span>;
iframe.width = <span class="number">420</span>;
iframe.height = <span class="number">600</span>;
iframe.style.border = <span class="string">'none'</span>;
iframe.style.borderRadius = <span class="string">'12px'</span>;
container.<span class="function">appendChild</span>(iframe);
<span class="comment">// Handle postMessage from iframe</span>
window.<span class="function">addEventListener</span>(<span class="string">'message'</span>, (event) => {
<span class="keyword">if</span> (event.origin === <span class="string">'https://auth.sonr.io'</span>) {
<span class="keyword">if</span> (event.data.type === <span class="string">'AUTH_SUCCESS'</span>) {
<span class="function">handleAuthSuccess</span>(event.data.payload);
} <span class="keyword">else if</span> (event.data.type === <span class="string">'AUTH_CANCEL'</span>) {
<span class="function">handleAuthCancel</span>();
}
}
});</pre>
</div>
</div>
</wa-tab-panel>
<wa-tab-panel name="redirect">
<div class="wa-stack wa-gap-m">
<p class="wa-caption-s" style="color: var(--wa-color-neutral-600);">
Standard OAuth 2.0 redirect flow for server-side applications.
</p>
<div class="code-block">
<pre><span class="comment">// Redirect to Sonr Auth (OAuth 2.0 flow)</span>
<span class="keyword">function</span> <span class="function">redirectToSonrAuth</span>() {
<span class="keyword">const</span> params = <span class="keyword">new</span> <span class="function">URLSearchParams</span>({
client_id: <span class="string">'YOUR_CLIENT_ID'</span>,
redirect_uri: <span class="string">'https://yourapp.com/callback'</span>,
response_type: <span class="string">'code'</span>,
scope: <span class="string">'openid profile wallet:read'</span>,
state: <span class="function">generateRandomState</span>(),
nonce: <span class="function">generateRandomNonce</span>()
});
window.location.href = <span class="string">`https://auth.sonr.io/authorize?${params}`</span>;
}
<span class="comment">// Handle callback on your server</span>
<span class="comment">// GET /callback?code=AUTH_CODE&state=STATE</span>
<span class="keyword">async function</span> <span class="function">handleCallback</span>(code, state) {
<span class="keyword">const</span> tokens = <span class="keyword">await</span> <span class="function">exchangeCodeForTokens</span>(code);
<span class="keyword">const</span> user = <span class="keyword">await</span> <span class="function">getUserInfo</span>(tokens.access_token);
<span class="keyword">return</span> { tokens, user };
}</pre>
</div>
</div>
</wa-tab-panel>
</wa-tab-group>
</wa-card>
</div>
</main>
</div>
<div class="popup-overlay" id="popup-overlay">
<div class="popup-window">
<div class="webview-wrapper">
<div class="webview-titlebar">
<div class="titlebar-buttons">
<div class="titlebar-btn close" id="popup-close"></div>
<div class="titlebar-btn minimize"></div>
<div class="titlebar-btn maximize"></div>
</div>
<div class="titlebar-url">
<wa-icon name="lock"></wa-icon>
<span id="popup-url-display">auth.sonr.io/welcome</span>
</div>
</div>
<iframe
id="popup-frame"
class="webview-frame"
src="welcome.html"
width="420"
height="600"
></iframe>
</div>
</div>
</div>
<script>
const viewSelect = document.getElementById('view-select');
const sizeSelect = document.getElementById('size-select');
const webviewFrame = document.getElementById('webview-frame');
const popupFrame = document.getElementById('popup-frame');
const urlDisplay = document.getElementById('url-display');
const popupUrlDisplay = document.getElementById('popup-url-display');
const sizeIndicator = document.getElementById('size-indicator');
const viewCards = document.querySelectorAll('.view-card');
const popupOverlay = document.getElementById('popup-overlay');
const popupClose = document.getElementById('popup-close');
const popupBtn = document.getElementById('popup-btn');
const newTabBtn = document.getElementById('new-tab-btn');
const views = {
welcome: { url: 'welcome.html', path: 'auth.sonr.io/welcome' },
login: { url: 'login.html', path: 'auth.sonr.io/login' },
register: { url: 'register.html', path: 'auth.sonr.io/register' },
authorize: { url: 'authorize.html', path: 'auth.sonr.io/authorize' }
};
const sizes = {
small: { width: 360, height: 540 },
medium: { width: 420, height: 600 },
large: { width: 480, height: 680 },
mobile: { width: 375, height: 667 }
};
function updateView(view) {
const viewData = views[view];
if (!viewData) return;
webviewFrame.src = viewData.url;
popupFrame.src = viewData.url;
urlDisplay.textContent = viewData.path;
popupUrlDisplay.textContent = viewData.path;
viewCards.forEach(card => {
card.classList.toggle('active', card.dataset.view === view);
});
viewSelect.value = view;
}
function updateSize(size) {
const sizeData = sizes[size];
if (!sizeData) return;
webviewFrame.width = sizeData.width;
webviewFrame.height = sizeData.height;
popupFrame.width = sizeData.width;
popupFrame.height = sizeData.height;
sizeIndicator.textContent = `${sizeData.width} × ${sizeData.height}`;
sizeSelect.value = size;
}
viewSelect.addEventListener('wa-change', (e) => {
updateView(e.target.value);
});
sizeSelect.addEventListener('wa-change', (e) => {
updateSize(e.target.value);
});
viewCards.forEach(card => {
card.addEventListener('click', () => {
updateView(card.dataset.view);
});
});
popupBtn.addEventListener('click', () => {
popupFrame.src = webviewFrame.src;
popupUrlDisplay.textContent = urlDisplay.textContent;
popupOverlay.classList.add('active');
});
popupClose.addEventListener('click', () => {
popupOverlay.classList.remove('active');
});
popupOverlay.addEventListener('click', (e) => {
if (e.target === popupOverlay) {
popupOverlay.classList.remove('active');
}
});
newTabBtn.addEventListener('click', () => {
const currentView = viewSelect.value;
window.open(views[currentView].url, '_blank');
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && popupOverlay.classList.contains('active')) {
popupOverlay.classList.remove('active');
}
});
updateView('welcome');
updateSize('medium');
</script>
</body>
</html>