Files
nebula/_migrate/authorize.html

720 lines
24 KiB
HTML
Raw 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: authorize.html → views/authorize.templ
================================================================================
PAGE OVERVIEW:
- OAuth/OIDC consent screen for third-party app authorization
- Three request types via tabs: Connect, Sign Message, Transaction
- Displays requesting app identity with verification badge
- Shows wallet selector and permission scopes
- Transaction tab includes swap preview, gas estimates, contract data
- VIEWPORT: Optimized for popup/webview (360×540 to 480×680)
VIEWPORT CONSTRAINTS (Auth Popup/Webview):
- Target sizes: 360×540 (small), 420×600 (medium), 480×680 (large)
- Card max-width: 48ch (~384px) fits all viewport sizes
- Tab panels scroll independently if content exceeds viewport
- Transaction details use compact layout on small screens
- Approve/Deny buttons always visible in footer
MAIN TEMPL COMPONENT:
templ AuthorizePage(req AuthRequest, wallet WalletInfo) {
@layouts.CenteredCard("Authorize - Sonr Motr Wallet") {
@AppIdentityHeader(req.App)
<wa-divider></wa-divider>
@RequestTypeTabs(req)
@AuthFooterActions(req.Type)
}
}
HTMX INTEGRATION:
- Tab switching: hx-on:wa-tab-show="htmx.ajax('GET', '/authorize/tab/' + event.detail.name, '#tab-content')"
- Approve action: hx-post="/api/authorize/approve" hx-include="[name='scopes']" hx-target="#auth-result"
- Deny action: hx-post="/api/authorize/deny" hx-target="#auth-result"
- Wallet selector: hx-get="/authorize/wallets" hx-target="#wallet-dropdown" hx-trigger="click"
SUB-COMPONENTS TO EXTRACT:
- AppIdentityHeader(app AppInfo) // Logo, name, verified badge, domain
- WalletSelector(wallets []Wallet, selected string)
- PermissionItem(icon string, iconClass string, title string, desc string)
- PermissionList(permissions []Permission)
- MessagePreview(message string, signType string)
- TransactionPreview(tx Transaction)
- SwapPreview(from TokenAmount, to TokenAmount)
- GasEstimate(network string, estimatedGas string, maxFee string)
- ContractDataDetails(contract string, function string, data string)
- AuthFooterActions(requestType string)
- AuthResultSuccess(type string, txHash string)
- AuthResultDenied()
STATE/PROPS:
type AuthRequest struct {
Type string // "connect", "sign", "transaction"
App AppInfo
Scopes []string
Message string // For sign requests
Transaction *TxDetails // For transaction requests
State string // OAuth state param
Nonce string // For OIDC
}
type AppInfo struct {
Name string
Domain string
LogoURL string
Verified bool
}
type TxDetails struct {
Type string // "swap", "send", "contract"
FromToken TokenAmount
ToToken TokenAmount
Network string
EstimatedGas string
MaxFee string
Contract string
Function string
RawData string
}
HTMX PATTERNS:
// Tab group with HTMX partial loading
<wa-tab-group hx-on:wa-tab-show="
htmx.ajax('GET', '/authorize/panel/' + event.detail.name, {target: '#panel-content'});
htmx.ajax('GET', '/authorize/button/' + event.detail.name, {target: '#approve-btn', swap: 'outerHTML'});
">
// Approve button with loading state
<wa-button id="approve-btn" variant="brand"
hx-post="/api/authorize/approve"
hx-include="[name='scopes'],[name='wallet']"
hx-target="#auth-card"
hx-indicator="this">
// Wallet selector dropdown via HTMX
<div class="wallet-selector"
hx-get="/authorize/wallet-options"
hx-target="#wallet-dropdown"
hx-trigger="click">
// Out-of-band result update (closes modal/redirects)
templ AuthResult(success bool, redirectURL string) {
<div id="auth-card" hx-swap-oob="true">
if success {
@AuthResultSuccess()
<script>setTimeout(() => window.location.href = "{ redirectURL }", 2000)</script>
} else {
@AuthResultDenied()
}
</div>
}
================================================================================
-->
<html lang="en" class="wa-cloak">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Allow Access - 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: 48ch;
}
/* App identity header */
.app-identity {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--wa-space-s);
text-align: center;
padding-bottom: var(--wa-space-m);
}
.app-logo {
width: 64px;
height: 64px;
border-radius: var(--wa-radius-l);
display: flex;
align-items: center;
justify-content: center;
background: var(--wa-color-surface-alt);
border: 1px solid var(--wa-color-neutral-200);
}
.app-logo img {
width: 48px;
height: 48px;
border-radius: var(--wa-radius-m);
}
.app-name {
display: flex;
align-items: center;
gap: var(--wa-space-2xs);
}
.verified-badge {
color: var(--wa-color-primary);
}
/* Wallet selector */
.wallet-selector {
background: var(--wa-color-surface-alt);
border-radius: var(--wa-radius-m);
padding: var(--wa-space-s) var(--wa-space-m);
}
.wallet-address {
font-family: var(--wa-font-mono);
font-size: var(--wa-font-size-s);
}
/* Permission items */
.permission-item {
display: flex;
align-items: flex-start;
gap: var(--wa-space-s);
padding: var(--wa-space-s) 0;
}
.permission-icon {
flex-shrink: 0;
width: 32px;
height: 32px;
border-radius: var(--wa-radius-s);
display: flex;
align-items: center;
justify-content: center;
}
.permission-icon.read {
background: var(--wa-color-success-subtle);
color: var(--wa-color-success);
}
.permission-icon.write {
background: var(--wa-color-warning-subtle);
color: var(--wa-color-warning);
}
.permission-icon.sign {
background: var(--wa-color-primary-subtle);
color: var(--wa-color-primary);
}
.permission-icon.danger {
background: var(--wa-color-danger-subtle);
color: var(--wa-color-danger);
}
/* Transaction preview */
.tx-preview {
background: var(--wa-color-surface-alt);
border-radius: var(--wa-radius-m);
padding: var(--wa-space-m);
font-family: var(--wa-font-mono);
font-size: var(--wa-font-size-s);
word-break: break-all;
max-height: 120px;
overflow-y: auto;
}
.tx-detail-row {
display: flex;
justify-content: space-between;
padding: var(--wa-space-xs) 0;
}
.tx-detail-row:not(:last-child) {
border-bottom: 1px solid var(--wa-color-neutral-200);
}
.tx-label {
color: var(--wa-color-neutral-600);
}
.tx-value {
font-family: var(--wa-font-mono);
font-size: var(--wa-font-size-s);
}
/* Actions footer */
.auth-actions {
display: flex;
gap: var(--wa-space-s);
padding-top: var(--wa-space-m);
}
.auth-actions wa-button {
flex: 1;
}
/* Tabs for different request types */
.request-tabs wa-tab-panel {
padding-top: var(--wa-space-m);
}
/* Raw data toggle */
.raw-data-section {
margin-top: var(--wa-space-m);
}
.raw-data-content {
background: var(--wa-color-neutral-900);
color: var(--wa-color-neutral-100);
border-radius: var(--wa-radius-m);
padding: var(--wa-space-m);
font-family: var(--wa-font-mono);
font-size: var(--wa-font-size-xs);
word-break: break-all;
max-height: 150px;
overflow-y: auto;
margin-top: var(--wa-space-s);
}
/* 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%;
}
.app-logo {
width: 48px;
height: 48px;
}
.app-logo img {
width: 36px;
height: 36px;
}
.permission-icon {
width: 28px;
height: 28px;
}
.wallet-selector {
padding: var(--wa-space-xs) var(--wa-space-s);
}
.tx-preview {
padding: var(--wa-space-s);
max-height: 100px;
}
.raw-data-content {
padding: var(--wa-space-s);
max-height: 100px;
}
}
@media (max-height: 600px) {
.app-identity {
padding-bottom: var(--wa-space-s);
}
.app-logo {
width: 48px;
height: 48px;
}
.permission-item {
padding: var(--wa-space-xs) 0;
}
.tx-preview {
max-height: 80px;
}
}
</style>
</head>
<body>
<wa-page>
<main class="main-centered">
<wa-card>
<!-- App Identity Header -->
<div class="app-identity">
<div class="app-logo">
<wa-icon name="cube" style="font-size: 32px; color: var(--wa-color-primary);"></wa-icon>
</div>
<div class="wa-stack wa-gap-2xs" style="align-items: center;">
<div class="app-name">
<span class="wa-heading-l">Uniswap</span>
<wa-icon name="badge-check" variant="solid" class="verified-badge"></wa-icon>
</div>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">app.uniswap.org</span>
</div>
</div>
<wa-divider></wa-divider>
<!-- Request Type Tabs -->
<wa-tab-group class="request-tabs">
<wa-tab panel="connect">
<div class="wa-cluster wa-gap-xs">
<wa-icon name="link"></wa-icon>
<span>Connect</span>
</div>
</wa-tab>
<wa-tab panel="sign">
<div class="wa-cluster wa-gap-xs">
<wa-icon name="pen-nib"></wa-icon>
<span>Sign</span>
</div>
</wa-tab>
<wa-tab panel="transaction">
<div class="wa-cluster wa-gap-xs">
<wa-icon name="paper-plane"></wa-icon>
<span>Transaction</span>
</div>
</wa-tab>
<!-- Connect Panel -->
<wa-tab-panel name="connect">
<div class="wa-stack">
<!-- Wallet Selection -->
<div class="wallet-selector">
<div class="wa-flank">
<div class="wa-cluster wa-gap-s">
<wa-avatar initials="S" style="--size: 36px;"></wa-avatar>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">Main Wallet</span>
<span class="wallet-address">sonr1x9f...7k2m</span>
</div>
</div>
<wa-icon-button name="chevron-down" variant="plain"></wa-icon-button>
</div>
</div>
<!-- Permissions List -->
<div class="wa-stack wa-gap-xs">
<span class="wa-heading-s">This app wants to:</span>
<div class="permission-item">
<div class="permission-icon read">
<wa-icon name="eye"></wa-icon>
</div>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-xs">See your wallet address</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Your public address only</span>
</div>
</div>
<div class="permission-item">
<div class="permission-icon read">
<wa-icon name="coins"></wa-icon>
</div>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-xs">See your balances</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Token amounts in your wallet</span>
</div>
</div>
<div class="permission-item">
<div class="permission-icon write">
<wa-icon name="bell"></wa-icon>
</div>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-xs">Ask to send transactions</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">You'll approve each one separately</span>
</div>
</div>
</div>
<!-- Warning Callout -->
<wa-callout variant="neutral">
<wa-icon slot="icon" name="shield-check"></wa-icon>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-xs">This app cannot:</span>
<span class="wa-caption-s">Spend your funds without asking you first.</span>
</div>
</wa-callout>
</div>
</wa-tab-panel>
<!-- Sign Message Panel -->
<wa-tab-panel name="sign">
<div class="wa-stack">
<!-- Signing Wallet -->
<div class="wallet-selector">
<div class="wa-flank">
<div class="wa-cluster wa-gap-s">
<wa-avatar initials="S" style="--size: 36px;"></wa-avatar>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">Main Wallet</span>
<span class="wallet-address">sonr1x9f...7k2m</span>
</div>
</div>
<wa-badge variant="neutral">Signing</wa-badge>
</div>
</div>
<!-- Message Preview -->
<div class="wa-stack wa-gap-xs">
<span class="wa-heading-s">You're signing this message:</span>
<div class="tx-preview">
Welcome to Uniswap!
Click to sign in and accept the Uniswap Terms of Service.
This request will not trigger a blockchain transaction or cost any gas fees.
Wallet address:
sonr1x9f4h2k8m3n5p7q2r4s6t8v0w3x5y7z9a1b3c5d7k2m
Nonce: 8f4a2b1c
</div>
</div>
<!-- Sign Type Info -->
<wa-callout variant="brand" appearance="filled">
<wa-icon slot="icon" name="signature"></wa-icon>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-xs">Free to sign</span>
<span class="wa-caption-s">No fees. This just proves you own this wallet.</span>
</div>
</wa-callout>
<!-- Advanced: Raw Data Toggle -->
<div class="raw-data-section">
<wa-details summary="Show technical details">
<div class="raw-data-content">
0x57656c636f6d6520746f20556e697377617021
436c69636b20746f207369676e20696e20616e642061636365707420746865
20556e6973776170205465726d73206f6620536572766963652e0a0a5468
69732072657175657374200a0a57616c6c65742061646472657373
</div>
</wa-details>
</div>
</div>
</wa-tab-panel>
<!-- Transaction Panel -->
<wa-tab-panel name="transaction">
<div class="wa-stack">
<!-- From Wallet -->
<div class="wallet-selector">
<div class="wa-flank">
<div class="wa-cluster wa-gap-s">
<wa-avatar initials="S" style="--size: 36px;"></wa-avatar>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">Main Wallet</span>
<span class="wallet-address">sonr1x9f...7k2m</span>
</div>
</div>
<div class="wa-stack wa-gap-0" style="text-align: right;">
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Balance</span>
<span class="wa-heading-s">1,234.56 SNR</span>
</div>
</div>
</div>
<!-- Transaction Details Card -->
<wa-card>
<div class="wa-stack wa-gap-s">
<div class="wa-flank">
<span class="wa-heading-m">Swap</span>
<wa-badge variant="warning">Waiting</wa-badge>
</div>
<wa-divider></wa-divider>
<!-- Swap Visual -->
<div class="wa-stack wa-gap-s">
<div class="wa-flank">
<div class="wa-cluster wa-gap-s">
<wa-avatar initials="E" style="--size: 32px; background: var(--wa-color-neutral-200);"></wa-avatar>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">100.00 ETH</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Ethereum</span>
</div>
</div>
<span class="wa-caption-s" style="color: var(--wa-color-danger);">-$234,567.00</span>
</div>
<div style="text-align: center;">
<wa-icon name="arrow-down" style="color: var(--wa-color-neutral-400);"></wa-icon>
</div>
<div class="wa-flank">
<div class="wa-cluster wa-gap-s">
<wa-avatar initials="U" style="--size: 32px; background: var(--wa-color-primary-subtle);"></wa-avatar>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">~125,000 USDC</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">USD Coin</span>
</div>
</div>
<span class="wa-caption-s" style="color: var(--wa-color-success);">+$125,000.00</span>
</div>
</div>
</div>
</wa-card>
<!-- Gas & Fee Details -->
<div class="wa-stack wa-gap-2xs">
<div class="tx-detail-row">
<span class="tx-label">Network</span>
<span class="tx-value">Sonr Mainnet</span>
</div>
<div class="tx-detail-row">
<span class="tx-label">Network fee</span>
<span class="tx-value">~$0.12</span>
</div>
<div class="tx-detail-row">
<span class="tx-label">Max fee</span>
<span class="tx-value">$0.26</span>
</div>
<div class="tx-detail-row">
<span class="tx-label">Price change limit</span>
<span class="tx-value">0.5%</span>
</div>
</div>
<!-- Danger Warning for High Value -->
<wa-callout variant="warning">
<wa-icon slot="icon" name="triangle-alert"></wa-icon>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-xs">Large amount</span>
<span class="wa-caption-s">Please review the details carefully before confirming.</span>
</div>
</wa-callout>
<!-- Advanced: Contract Data -->
<div class="raw-data-section">
<wa-details summary="Show technical details">
<div class="wa-stack wa-gap-s">
<div class="tx-detail-row">
<span class="tx-label">Contract</span>
<span class="tx-value">0x7a25...3f8b</span>
</div>
<div class="tx-detail-row">
<span class="tx-label">Function</span>
<span class="tx-value">swapExactTokensForTokens()</span>
</div>
<div class="raw-data-content">
0x38ed1739
0000000000000000000000000000000000000056bc75e2d63100000
000000000000000000000000000000000000001e8480
00000000000000000000000000000000000000000000000000000060
0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659
</div>
</div>
</wa-details>
</div>
</div>
</wa-tab-panel>
</wa-tab-group>
<!-- Footer Actions -->
<div slot="footer">
<div class="wa-stack wa-gap-s">
<div class="auth-actions">
<wa-button variant="neutral" appearance="outlined" id="deny-btn">
<wa-icon slot="start" name="x"></wa-icon>
Cancel
</wa-button>
<wa-button variant="brand" id="approve-btn">
<wa-icon slot="start" name="check"></wa-icon>
Allow
</wa-button>
</div>
<div style="text-align: center;">
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">
<wa-icon name="lock" style="font-size: 12px;"></wa-icon>
Protected by Sonr
</span>
</div>
</div>
</div>
</wa-card>
</main>
</wa-page>
<script>
// Tab panel state management
const tabGroup = document.querySelector('wa-tab-group');
const approveBtn = document.getElementById('approve-btn');
const denyBtn = document.getElementById('deny-btn');
// Update button text based on active panel
tabGroup.addEventListener('wa-tab-show', (event) => {
const panel = event.detail.name;
switch (panel) {
case 'connect':
approveBtn.innerHTML = '<wa-icon slot="start" name="link"></wa-icon>Allow';
break;
case 'sign':
approveBtn.innerHTML = '<wa-icon slot="start" name="pen-nib"></wa-icon>Sign';
break;
case 'transaction':
approveBtn.innerHTML = '<wa-icon slot="start" name="paper-plane"></wa-icon>Confirm';
break;
}
});
// Simulated approve action
approveBtn.addEventListener('click', async () => {
const activePanel = tabGroup.querySelector('wa-tab[active]')?.getAttribute('panel') || 'connect';
approveBtn.loading = true;
approveBtn.disabled = true;
denyBtn.disabled = true;
// Simulate WebAuthn authentication
await new Promise(resolve => setTimeout(resolve, 1500));
// Show success
const card = document.querySelector('wa-card');
card.innerHTML = `
<div class="wa-stack" style="text-align: center; padding: var(--wa-space-xl) 0;">
<wa-icon name="circle-check" variant="solid" style="font-size: 64px; color: var(--wa-color-success);"></wa-icon>
<span class="wa-heading-l">${activePanel === 'connect' ? 'Connected' : activePanel === 'sign' ? 'Signed' : 'Sent'}</span>
<span class="wa-caption-m" style="color: var(--wa-color-neutral-500);">
${activePanel === 'transaction' ? 'Transaction ID: 0x8f4a2b1c...' : 'You can close this window.'}
</span>
<wa-button variant="neutral" appearance="outlined" onclick="window.close()">
Close
</wa-button>
</div>
`;
});
// Deny action
denyBtn.addEventListener('click', () => {
const card = document.querySelector('wa-card');
card.innerHTML = `
<div class="wa-stack" style="text-align: center; padding: var(--wa-space-xl) 0;">
<wa-icon name="circle-x" variant="solid" style="font-size: 64px; color: var(--wa-color-danger);"></wa-icon>
<span class="wa-heading-l">Cancelled</span>
<span class="wa-caption-m" style="color: var(--wa-color-neutral-500);">
No access was granted.
</span>
<wa-button variant="neutral" appearance="outlined" onclick="window.close()">
Close
</wa-button>
</div>
`;
});
</script>
</body>
</html>