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/step/{step}", handleLoginStep)
|
||||
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
|
||||
@@ -122,3 +128,82 @@ func handleLoginQRStatus(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
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