Files
nebula/views/authorize.templ

562 lines
15 KiB
Plaintext

package views
import (
"nebula/layouts"
"nebula/models"
)
templ AuthorizePage(req models.AuthRequest) {
@layouts.CenteredCard("Authorize - Sonr") {
<div id="auth-content">
@AuthorizeContent(req)
</div>
<div id="htmx-indicator" class="htmx-indicator">
<wa-spinner></wa-spinner>
</div>
}
}
templ AuthorizeContent(req models.AuthRequest) {
@authorizeStyles()
@AppIdentityHeader(req.App)
<wa-divider></wa-divider>
@RequestTypeTabs(req)
<footer slot="footer">
@AuthFooterActions(req.Type)
</footer>
}
templ AppIdentityHeader(app models.AppInfo) {
<div class="app-identity">
<div class="app-logo">
<wa-icon name={ app.LogoIcon } 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">{ app.Name }</span>
if app.Verified {
<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.Domain }</span>
</div>
</div>
}
templ RequestTypeTabs(req models.AuthRequest) {
<wa-tab-group class="request-tabs" id="auth-tabs">
<wa-tab panel="connect" if req.Type == "connect" || req.Type == "" {
active?={ true }
}>
<div class="wa-cluster wa-gap-xs">
<wa-icon name="link"></wa-icon>
<span>Connect</span>
</div>
</wa-tab>
<wa-tab panel="sign" if req.Type == "sign" {
active?={ true }
}>
<div class="wa-cluster wa-gap-xs">
<wa-icon name="pen-nib"></wa-icon>
<span>Sign</span>
</div>
</wa-tab>
<wa-tab panel="transaction" if req.Type == "transaction" {
active?={ true }
}>
<div class="wa-cluster wa-gap-xs">
<wa-icon name="paper-plane"></wa-icon>
<span>Transaction</span>
</div>
</wa-tab>
<wa-tab-panel name="connect">
@ConnectPanel(req.Wallet)
</wa-tab-panel>
<wa-tab-panel name="sign">
@SignPanel(req.Wallet, req.Message, req.MessageHex)
</wa-tab-panel>
<wa-tab-panel name="transaction">
@TransactionPanel(req.Wallet, req.Transaction)
</wa-tab-panel>
</wa-tab-group>
@tabScripts()
}
templ ConnectPanel(wallet models.WalletInfo) {
<div class="wa-stack wa-gap-m">
@WalletSelector(wallet, false, "")
<div class="wa-stack wa-gap-xs">
<span class="wa-heading-s">This app wants to:</span>
@PermissionItem("eye", "read", "See your wallet address", "Your public address only")
@PermissionItem("coins", "read", "See your balances", "Token amounts in your wallet")
@PermissionItem("bell", "write", "Ask to send transactions", "You'll approve each one separately")
</div>
<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>
}
templ SignPanel(wallet models.WalletInfo, message string, messageHex string) {
<div class="wa-stack wa-gap-m">
@WalletSelector(wallet, true, "Signing")
<div class="wa-stack wa-gap-xs">
<span class="wa-heading-s">You're signing this message:</span>
<div class="tx-preview">{ message }</div>
</div>
<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>
if messageHex != "" {
<div class="raw-data-section">
<wa-details summary="Show technical details">
<div class="raw-data-content">{ messageHex }</div>
</wa-details>
</div>
}
</div>
}
templ TransactionPanel(wallet models.WalletInfo, tx *models.TxDetails) {
<div class="wa-stack wa-gap-m">
@WalletSelectorWithBalance(wallet)
if tx != nil {
@TransactionCard(tx)
@TransactionDetails(tx)
<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>
<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">{ tx.Contract }</span>
</div>
<div class="tx-detail-row">
<span class="tx-label">Function</span>
<span class="tx-value">{ tx.Function }</span>
</div>
<div class="raw-data-content">{ tx.RawData }</div>
</div>
</wa-details>
</div>
}
</div>
}
templ WalletSelector(wallet models.WalletInfo, showBadge bool, badgeText string) {
<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">{ wallet.Name }</span>
<span class="wallet-address">{ wallet.Address }</span>
</div>
</div>
if showBadge && badgeText != "" {
<wa-badge variant="neutral">{ badgeText }</wa-badge>
} else {
<wa-icon-button name="chevron-down" variant="plain"></wa-icon-button>
}
</div>
</div>
}
templ WalletSelectorWithBalance(wallet models.WalletInfo) {
<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">{ wallet.Name }</span>
<span class="wallet-address">{ wallet.Address }</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">{ wallet.Balance }</span>
</div>
</div>
</div>
}
templ PermissionItem(icon string, variant string, title string, description string) {
<div class="permission-item">
<div class={ "permission-icon", variant }>
<wa-icon name={ icon }></wa-icon>
</div>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-xs">{ title }</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">{ description }</span>
</div>
</div>
}
templ TransactionCard(tx *models.TxDetails) {
<wa-card>
<div class="wa-stack wa-gap-s">
<div class="wa-flank">
<span class="wa-heading-m">{ tx.Type }</span>
<wa-badge variant="warning">Waiting</wa-badge>
</div>
<wa-divider></wa-divider>
<div class="wa-stack wa-gap-s">
<div class="wa-flank">
<div class="wa-cluster wa-gap-s">
<wa-avatar initials={ tx.FromToken.Initials } style="--size: 32px; background: var(--wa-color-neutral-200);"></wa-avatar>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">{ tx.FromToken.Amount } { tx.FromToken.Symbol }</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">{ tx.FromToken.Symbol }</span>
</div>
</div>
<span class="wa-caption-s" style="color: var(--wa-color-danger);">-{ tx.FromToken.USD }</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={ tx.ToToken.Initials } style="--size: 32px; background: var(--wa-color-primary-subtle);"></wa-avatar>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">~{ tx.ToToken.Amount } { tx.ToToken.Symbol }</span>
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">{ tx.ToToken.Symbol }</span>
</div>
</div>
<span class="wa-caption-s" style="color: var(--wa-color-success);">+{ tx.ToToken.USD }</span>
</div>
</div>
</div>
</wa-card>
}
templ TransactionDetails(tx *models.TxDetails) {
<div class="wa-stack wa-gap-2xs">
<div class="tx-detail-row">
<span class="tx-label">Network</span>
<span class="tx-value">{ tx.Network }</span>
</div>
<div class="tx-detail-row">
<span class="tx-label">Network fee</span>
<span class="tx-value">{ tx.NetworkFee }</span>
</div>
<div class="tx-detail-row">
<span class="tx-label">Max fee</span>
<span class="tx-value">{ tx.MaxFee }</span>
</div>
<div class="tx-detail-row">
<span class="tx-label">Price change limit</span>
<span class="tx-value">{ tx.Slippage }</span>
</div>
</div>
}
templ AuthFooterActions(requestType string) {
<div class="wa-stack wa-gap-s">
<div class="auth-actions">
<wa-button
variant="neutral"
appearance="outlined"
id="deny-btn"
hx-post="/authorize/deny"
hx-target="#auth-content"
hx-swap="innerHTML transition:true"
hx-indicator="#htmx-indicator"
>
<wa-icon slot="start" name="x"></wa-icon>
Cancel
</wa-button>
<wa-button
variant="brand"
id="approve-btn"
hx-post="/authorize/approve"
hx-target="#auth-content"
hx-swap="innerHTML transition:true"
hx-indicator="#htmx-indicator"
hx-vals={ `{"type":"` + requestType + `"}` }
>
<wa-icon slot="start" name="check"></wa-icon>
if requestType == "sign" {
Sign
} else if requestType == "transaction" {
Confirm
} else {
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>
}
templ AuthResultSuccess(actionType string) {
@authorizeStyles()
<div class="wa-stack wa-gap-l" 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">
if actionType == "sign" {
Signed
} else if actionType == "transaction" {
Sent
} else {
Connected
}
</span>
<span class="wa-caption-m" style="color: var(--wa-color-neutral-500);">
if actionType == "transaction" {
Transaction ID: 0x8f4a2b1c...
} else {
You can close this window.
}
</span>
<wa-button variant="neutral" appearance="outlined" onclick="window.close()">
Close
</wa-button>
</div>
}
templ AuthResultDenied() {
@authorizeStyles()
<div class="wa-stack wa-gap-l" 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>
}
templ tabScripts() {
<script>
(function() {
const tabGroup = document.getElementById('auth-tabs');
const approveBtn = document.getElementById('approve-btn');
if (tabGroup && approveBtn) {
tabGroup.addEventListener('wa-tab-show', (event) => {
const panel = event.detail.name;
let icon, text;
switch (panel) {
case 'connect':
icon = 'link';
text = 'Allow';
break;
case 'sign':
icon = 'pen-nib';
text = 'Sign';
break;
case 'transaction':
icon = 'paper-plane';
text = 'Confirm';
break;
}
approveBtn.innerHTML = `<wa-icon slot="start" name="${icon}"></wa-icon>${text}`;
approveBtn.setAttribute('hx-vals', JSON.stringify({type: panel}));
htmx.process(approveBtn);
});
}
})();
</script>
}
templ authorizeStyles() {
<style>
.htmx-indicator {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 100;
}
.htmx-request .htmx-indicator {
display: flex;
}
.htmx-request #auth-content {
opacity: 0.5;
pointer-events: none;
transition: opacity 0.2s ease;
}
@view-transition {
navigation: auto;
}
.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-name {
display: flex;
align-items: center;
gap: var(--wa-space-2xs);
}
.verified-badge {
color: var(--wa-color-primary);
}
.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);
color: var(--wa-color-neutral-500);
}
.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);
}
.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;
white-space: pre-wrap;
}
.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);
}
.auth-actions {
display: flex;
gap: var(--wa-space-s);
}
.auth-actions wa-button {
flex: 1;
}
.request-tabs wa-tab-panel {
padding-top: var(--wa-space-m);
}
.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);
}
@media (max-width: 400px) {
.app-logo {
width: 48px;
height: 48px;
}
.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>
}