feat(did): add web auth for oidc authorization flows
This commit is contained in:
@@ -1,719 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -20,6 +20,12 @@ func RegisterRoutes(mux *http.ServeMux) {
|
|||||||
mux.HandleFunc("GET /login", handleLogin)
|
mux.HandleFunc("GET /login", handleLogin)
|
||||||
mux.HandleFunc("GET /login/step/{step}", handleLoginStep)
|
mux.HandleFunc("GET /login/step/{step}", handleLoginStep)
|
||||||
mux.HandleFunc("GET /login/qr-status", handleLoginQRStatus)
|
mux.HandleFunc("GET /login/qr-status", handleLoginQRStatus)
|
||||||
|
|
||||||
|
mux.HandleFunc("GET /authorize", handleAuthorize)
|
||||||
|
mux.HandleFunc("POST /authorize/approve", handleAuthorizeApprove)
|
||||||
|
mux.HandleFunc("POST /authorize/deny", handleAuthorizeDeny)
|
||||||
|
|
||||||
|
mux.HandleFunc("GET /dashboard", handleDashboard)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleWelcome renders the full welcome page at step 1
|
// handleWelcome renders the full welcome page at step 1
|
||||||
@@ -122,3 +128,82 @@ func handleLoginQRStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
views.QRStatusWaiting().Render(r.Context(), w)
|
views.QRStatusWaiting().Render(r.Context(), w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleAuthorize(w http.ResponseWriter, r *http.Request) {
|
||||||
|
reqType := r.URL.Query().Get("type")
|
||||||
|
if reqType == "" {
|
||||||
|
reqType = "connect"
|
||||||
|
}
|
||||||
|
|
||||||
|
req := views.AuthRequest{
|
||||||
|
Type: reqType,
|
||||||
|
App: views.AppInfo{
|
||||||
|
Name: "Uniswap",
|
||||||
|
Domain: "app.uniswap.org",
|
||||||
|
LogoIcon: "cube",
|
||||||
|
Verified: true,
|
||||||
|
},
|
||||||
|
Wallet: views.WalletInfo{
|
||||||
|
Name: "Main Wallet",
|
||||||
|
Address: "sonr1x9f...7k2m",
|
||||||
|
Balance: "1,234.56 SNR",
|
||||||
|
},
|
||||||
|
Message: `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`,
|
||||||
|
MessageHex: "0x57656c636f6d6520746f20556e697377617021...",
|
||||||
|
Transaction: &views.TxDetails{
|
||||||
|
Type: "Swap",
|
||||||
|
FromToken: views.TokenAmount{
|
||||||
|
Symbol: "ETH",
|
||||||
|
Amount: "100.00",
|
||||||
|
USD: "$234,567.00",
|
||||||
|
Initials: "E",
|
||||||
|
},
|
||||||
|
ToToken: views.TokenAmount{
|
||||||
|
Symbol: "USDC",
|
||||||
|
Amount: "125,000",
|
||||||
|
USD: "$125,000.00",
|
||||||
|
Initials: "U",
|
||||||
|
},
|
||||||
|
Network: "Sonr Mainnet",
|
||||||
|
NetworkFee: "~$0.12",
|
||||||
|
MaxFee: "$0.26",
|
||||||
|
Slippage: "0.5%",
|
||||||
|
Contract: "0x7a25...3f8b",
|
||||||
|
Function: "swapExactTokensForTokens()",
|
||||||
|
RawData: "0x38ed1739\n0000000000000000000000000000000000000056bc75e2d63100000...",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
views.AuthorizePage(req).Render(r.Context(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAuthorizeApprove(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.ParseForm()
|
||||||
|
actionType := r.FormValue("type")
|
||||||
|
if actionType == "" {
|
||||||
|
actionType = "connect"
|
||||||
|
}
|
||||||
|
views.AuthResultSuccess(actionType).Render(r.Context(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAuthorizeDeny(w http.ResponseWriter, r *http.Request) {
|
||||||
|
views.AuthResultDenied().Render(r.Context(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tab := r.URL.Query().Get("tab")
|
||||||
|
if tab == "" {
|
||||||
|
tab = "accounts"
|
||||||
|
}
|
||||||
|
data := views.DefaultDashboardData()
|
||||||
|
views.DashboardPage(data, tab).Render(r.Context(), w)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user