562 lines
15 KiB
Plaintext
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>
|
|
}
|