Files
nebula/_migrate/demo.html

655 lines
23 KiB
HTML
Raw Normal View History

<!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>