Files
nebula/_migrate/service.html

1339 lines
44 KiB
HTML

<!DOCTYPE html>
<!--
================================================================================
TEMPL MIGRATION GUIDE: service.html → views/service.templ
================================================================================
PAGE OVERVIEW:
- Connected service detail page showing comprehensive information about an OAuth client/dApp
- Hero section: Service logo, name, domain, verification status, connection date, action buttons
- Usage stats: Sessions, signatures, transactions approved, permissions used
- Permissions management: Granular scope controls with descriptions
- Session log: History of all authentication events and actions
- Security info: OAuth client ID, redirect URIs, token expiry
- Similar to Google Account connected apps or GitHub OAuth app detail pages
MAIN TEMPL COMPONENT:
templ ServicePage(service ServiceData) {
@layouts.DashboardLayout("connections") {
@ServiceHero(service.Info, service.Status)
@UsageStatsGrid(service.Stats)
<div class="wa-grid">
@PermissionsCard(service.Permissions)
@ServiceInfoCard(service.OAuth)
</div>
@SessionLogCard(service.Sessions)
@DataAccessCard(service.DataAccess)
}
}
HTMX INTEGRATION:
- Revoke access: hx-delete="/api/connections/{id}" hx-confirm="Disconnect this app?" hx-target="body" hx-push-url="/connections"
- Toggle permission: hx-post="/api/connections/{id}/scopes" hx-vals='{"scope":"wallet:sign","enabled":false}' hx-target="#permission-status"
- Revoke all permissions: hx-post="/api/connections/{id}/revoke-all" hx-target="#permissions-list"
- Refresh token: hx-post="/api/connections/{id}/refresh-token" hx-target="#token-info"
- Session log refresh: hx-get="/api/connections/{id}/sessions" hx-trigger="every 30s" hx-target="#session-log"
- Revoke session: hx-delete="/api/connections/{id}/sessions/{sessionId}" hx-target="closest .session-item" hx-swap="delete"
- Block service: hx-post="/api/connections/{id}/block" hx-target="#service-status"
SUB-COMPONENTS TO EXTRACT:
- ServiceHero(info ServiceInfo, status ServiceStatus)
- ServiceLogo(logoUrl string, name string, color string, size string)
- ServiceStatusBadge(isVerified bool, isActive bool)
- UsageStatsGrid(stats UsageStats)
- StatCard(label string, value string, icon string, trend string)
- PermissionsCard(permissions []Permission)
- PermissionItem(permission Permission)
- PermissionToggle(scope string, enabled bool, description string)
- ServiceInfoCard(oauth OAuthInfo)
- OAuthDisplay(clientId string, redirectUris []string)
- TokenInfo(issuedAt time.Time, expiresAt time.Time)
- SessionLogCard(sessions []SessionEvent)
- SessionLogItem(event SessionEvent)
- DataAccessCard(access DataAccess)
- DataAccessItem(type string, count int, lastAccessed time.Time)
STATE/PROPS:
type ServiceData struct {
Info ServiceInfo
Status ServiceStatus
Stats UsageStats
Permissions []Permission
OAuth OAuthInfo
Sessions []SessionEvent
DataAccess DataAccess
}
type ServiceInfo struct {
ID string
Name string
Domain string
Description string
LogoURL string
LogoColor string
IconName string
Category string // "defi", "nft", "social", "gaming", "utility"
Website string
PrivacyURL string
TermsURL string
SupportEmail string
}
type ServiceStatus struct {
IsVerified bool
IsActive bool
IsBlocked bool
ConnectedAt time.Time
LastUsed time.Time
ExpiresAt time.Time
}
type UsageStats struct {
TotalSessions int
ActiveSessions int
SignaturesTotal int
Signatures30d int
TxApproved int
TxRejected int
PermissionsUsed int
PermissionsTotal int
DataRequests int
}
type Permission struct {
Scope string // "openid", "profile", "wallet:read", "wallet:sign", "wallet:transact"
Name string
Description string
Type string // "required", "optional"
Enabled bool
GrantedAt time.Time
LastUsed time.Time
UsageCount int
}
type OAuthInfo struct {
ClientID string
RedirectURIs []string
TokenType string // "Bearer"
IssuedAt time.Time
ExpiresAt time.Time
RefreshToken bool
Scopes []string
}
type SessionEvent struct {
ID string
Type string // "auth", "sign", "transaction", "data_request", "token_refresh", "logout"
Action string
Timestamp time.Time
IP string
Location string
UserAgent string
Status string // "success", "failed", "pending"
Details string
TxHash string
}
type DataAccess struct {
ProfileAccessed int
BalanceChecks int
TransactionReads int
NFTReads int
LastDataRequest time.Time
}
HTMX PATTERNS:
// Permission toggle
<wa-switch hx-post="/api/connections/{id}/scopes"
hx-vals='{"scope": "wallet:sign", "enabled": !this.checked}'
hx-target="#permission-status"
hx-swap="innerHTML">
// Disconnect service
<wa-button hx-delete="/api/connections/{id}"
hx-confirm="Disconnect {service.Name}? This will revoke all permissions and end all sessions."
hx-target="body"
hx-push-url="/connections">
Disconnect
</wa-button>
// Revoke individual session
<wa-button hx-delete="/api/connections/{id}/sessions/{session.ID}"
hx-target="closest .session-item"
hx-swap="delete"
hx-confirm="End this session?">
Revoke
</wa-button>
// Session log auto-refresh
<div id="session-log"
hx-get="/api/connections/{id}/sessions"
hx-trigger="every 30s"
hx-swap="innerHTML">
// Reset all permissions to required-only
<wa-button hx-post="/api/connections/{id}/reset-permissions"
hx-target="#permissions-list"
hx-confirm="Reset to minimum permissions?">
Reset Permissions
</wa-button>
================================================================================
-->
<html lang="en" class="wa-cloak">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Uniswap - Sonr Motr Wallet</title>
<script src="https://cdn.sonr.org/wa/autoloader.js"></script>
<style>
:root {
--wa-color-primary: #17c2ff;
--service-color: #ff007a;
}
html, body {
min-height: 100%;
padding: 0;
margin: 0;
}
.dashboard-layout {
display: grid;
grid-template-columns: 240px 1fr;
min-height: 100vh;
}
@media (max-width: 768px) {
.dashboard-layout {
grid-template-columns: 1fr;
}
.sidebar {
display: none;
}
}
.sidebar {
border-right: 1px solid var(--wa-color-neutral-200);
padding: var(--wa-space-m);
background: var(--wa-color-surface);
}
.sidebar-header {
padding: var(--wa-space-s) var(--wa-space-xs);
margin-bottom: var(--wa-space-m);
}
.sidebar-nav {
list-style: none;
padding: 0;
margin: 0;
}
.sidebar-nav li {
margin-bottom: var(--wa-space-2xs);
}
.sidebar-nav a {
display: flex;
align-items: center;
gap: var(--wa-space-s);
padding: var(--wa-space-s) var(--wa-space-m);
border-radius: var(--wa-radius-m);
text-decoration: none;
color: var(--wa-color-neutral-700);
font-size: var(--wa-font-size-s);
transition: background 0.15s;
}
.sidebar-nav a:hover {
background: var(--wa-color-surface-alt);
}
.sidebar-nav a.active {
background: var(--wa-color-primary-subtle);
color: var(--wa-color-primary);
font-weight: 500;
}
.main-content {
padding: var(--wa-space-xl);
background: var(--wa-color-surface-alt);
overflow-y: auto;
}
/* Service Hero Section */
.service-hero {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: flex-start;
gap: var(--wa-space-xl);
margin-bottom: var(--wa-space-xl);
padding: var(--wa-space-xl);
background: var(--wa-color-surface);
border-radius: var(--wa-radius-l);
}
.service-identity {
display: flex;
align-items: center;
gap: var(--wa-space-l);
}
.service-logo {
width: 72px;
height: 72px;
border-radius: var(--wa-radius-l);
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: white;
background: linear-gradient(135deg, var(--service-color) 0%, color-mix(in srgb, var(--service-color) 70%, black) 100%);
overflow: hidden;
}
.service-logo img {
width: 100%;
height: 100%;
object-fit: cover;
}
.service-name-block {
display: flex;
flex-direction: column;
gap: var(--wa-space-2xs);
}
.service-badges {
display: flex;
gap: var(--wa-space-xs);
margin-top: var(--wa-space-2xs);
}
.service-description {
font-size: var(--wa-font-size-s);
color: var(--wa-color-neutral-600);
max-width: 400px;
margin-top: var(--wa-space-s);
}
.status-block {
text-align: right;
}
.connection-info {
display: flex;
flex-direction: column;
gap: var(--wa-space-2xs);
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-500);
text-align: right;
margin-top: var(--wa-space-s);
}
.hero-actions {
display: flex;
gap: var(--wa-space-s);
width: 100%;
margin-top: var(--wa-space-l);
padding-top: var(--wa-space-l);
border-top: 1px solid var(--wa-color-neutral-100);
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: var(--wa-space-m);
margin-bottom: var(--wa-space-xl);
}
.stat-item {
background: var(--wa-color-surface);
padding: var(--wa-space-m);
border-radius: var(--wa-radius-m);
}
.stat-icon {
width: 36px;
height: 36px;
border-radius: var(--wa-radius-s);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
margin-bottom: var(--wa-space-s);
}
.stat-icon.success { background: var(--wa-color-success-subtle); color: var(--wa-color-success); }
.stat-icon.danger { background: var(--wa-color-danger-subtle); color: var(--wa-color-danger); }
.stat-icon.warning { background: var(--wa-color-warning-subtle); color: var(--wa-color-warning); }
.stat-icon.info { background: var(--wa-color-primary-subtle); color: var(--wa-color-primary); }
.stat-icon.neutral { background: var(--wa-color-neutral-100); color: var(--wa-color-neutral-600); }
.stat-label {
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-500);
margin-bottom: var(--wa-space-2xs);
}
.stat-value {
font-size: var(--wa-font-size-l);
font-weight: 600;
}
.stat-trend {
font-size: var(--wa-font-size-xs);
margin-top: var(--wa-space-2xs);
}
.stat-trend.up { color: var(--wa-color-success); }
.stat-trend.down { color: var(--wa-color-danger); }
/* Content Grid */
.content-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--wa-space-xl);
margin-bottom: var(--wa-space-xl);
}
@media (max-width: 1024px) {
.content-grid {
grid-template-columns: 1fr;
}
}
/* Permissions Card */
.permissions-list {
list-style: none;
padding: 0;
margin: 0;
}
.permission-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--wa-space-m);
padding: var(--wa-space-m);
border-bottom: 1px solid var(--wa-color-neutral-100);
}
.permission-item:last-child {
border-bottom: none;
}
.permission-info {
flex: 1;
min-width: 0;
}
.permission-header {
display: flex;
align-items: center;
gap: var(--wa-space-xs);
margin-bottom: var(--wa-space-2xs);
}
.permission-name {
font-size: var(--wa-font-size-s);
font-weight: 500;
}
.permission-scope {
font-family: var(--wa-font-mono);
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-500);
background: var(--wa-color-surface-alt);
padding: var(--wa-space-3xs) var(--wa-space-xs);
border-radius: var(--wa-radius-s);
}
.permission-description {
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-600);
margin-bottom: var(--wa-space-xs);
}
.permission-usage {
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-500);
}
/* OAuth Info Card */
.oauth-display {
background: var(--wa-color-surface-alt);
padding: var(--wa-space-m);
border-radius: var(--wa-radius-m);
margin-bottom: var(--wa-space-m);
}
.oauth-label {
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-500);
margin-bottom: var(--wa-space-2xs);
display: block;
}
.oauth-value {
font-family: var(--wa-font-mono);
font-size: var(--wa-font-size-xs);
word-break: break-all;
}
.oauth-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--wa-space-m);
}
.oauth-item {
display: flex;
flex-direction: column;
gap: var(--wa-space-2xs);
}
.oauth-item-label {
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-500);
}
.oauth-item-value {
font-size: var(--wa-font-size-s);
font-weight: 500;
}
.redirect-uris {
list-style: none;
padding: 0;
margin: var(--wa-space-s) 0 0 0;
}
.redirect-uris li {
font-family: var(--wa-font-mono);
font-size: var(--wa-font-size-xs);
padding: var(--wa-space-xs);
background: var(--wa-color-surface-alt);
border-radius: var(--wa-radius-s);
margin-bottom: var(--wa-space-xs);
word-break: break-all;
}
/* Session Log */
.session-log-list {
list-style: none;
padding: 0;
margin: 0;
max-height: 400px;
overflow-y: auto;
}
.session-item {
display: flex;
gap: var(--wa-space-m);
padding: var(--wa-space-m);
border-bottom: 1px solid var(--wa-color-neutral-100);
}
.session-item:last-child {
border-bottom: none;
}
.session-icon {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
}
.session-icon.auth { background: var(--wa-color-primary-subtle); color: var(--wa-color-primary); }
.session-icon.sign { background: var(--wa-color-warning-subtle); color: var(--wa-color-warning); }
.session-icon.transaction { background: var(--wa-color-success-subtle); color: var(--wa-color-success); }
.session-icon.data { background: var(--wa-color-neutral-100); color: var(--wa-color-neutral-600); }
.session-icon.error { background: var(--wa-color-danger-subtle); color: var(--wa-color-danger); }
.session-content {
flex: 1;
min-width: 0;
}
.session-title {
font-size: var(--wa-font-size-s);
font-weight: 500;
margin-bottom: var(--wa-space-3xs);
}
.session-meta {
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-500);
}
.session-details {
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-600);
margin-top: var(--wa-space-2xs);
}
.session-tx-hash {
font-family: var(--wa-font-mono);
font-size: var(--wa-font-size-xs);
color: var(--wa-color-primary);
text-decoration: none;
}
.session-tx-hash:hover {
text-decoration: underline;
}
/* Data Access Card */
.data-access-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: var(--wa-space-m);
}
.data-access-item {
background: var(--wa-color-surface-alt);
padding: var(--wa-space-m);
border-radius: var(--wa-radius-m);
text-align: center;
}
.data-access-count {
font-size: var(--wa-font-size-xl);
font-weight: 700;
color: var(--wa-color-neutral-700);
}
.data-access-label {
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-500);
margin-top: var(--wa-space-2xs);
}
/* Back link */
.back-link {
display: inline-flex;
align-items: center;
gap: var(--wa-space-xs);
color: var(--wa-color-neutral-500);
text-decoration: none;
font-size: var(--wa-font-size-s);
margin-bottom: var(--wa-space-m);
transition: color 0.15s;
}
.back-link:hover {
color: var(--wa-color-primary);
}
/* Resource links */
.resource-links {
display: flex;
flex-wrap: wrap;
gap: var(--wa-space-s);
margin-top: var(--wa-space-m);
}
.resource-link {
display: inline-flex;
align-items: center;
gap: var(--wa-space-2xs);
padding: var(--wa-space-xs) var(--wa-space-s);
background: var(--wa-color-surface-alt);
border-radius: var(--wa-radius-s);
font-size: var(--wa-font-size-xs);
color: var(--wa-color-neutral-600);
text-decoration: none;
transition: background 0.15s, color 0.15s;
}
.resource-link:hover {
background: var(--wa-color-primary-subtle);
color: var(--wa-color-primary);
}
/* Filter tabs */
.filter-tabs {
margin-bottom: var(--wa-space-m);
}
</style>
</head>
<body>
<wa-page>
<div class="dashboard-layout">
<aside class="sidebar">
<div class="sidebar-header">
<div class="wa-cluster wa-gap-s">
<wa-avatar initials="S" style="--size: 32px; background: var(--wa-color-primary);"></wa-avatar>
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">Sonr Wallet</span>
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">sonr1x9f...7k2m</span>
</div>
</div>
</div>
<nav>
<ul class="sidebar-nav">
<li>
<a href="accounts.html">
<wa-icon name="wallet"></wa-icon>
Accounts
</a>
</li>
<li>
<a href="transactions.html">
<wa-icon name="arrow-right-arrow-left"></wa-icon>
Transactions
</a>
</li>
<li>
<a href="tokens.html">
<wa-icon name="coins"></wa-icon>
Tokens
</a>
</li>
<li>
<a href="nfts.html">
<wa-icon name="image"></wa-icon>
NFTs
</a>
</li>
<li>
<a href="activity.html">
<wa-icon name="chart-line"></wa-icon>
Activity
</a>
</li>
</ul>
</nav>
<wa-divider style="margin: var(--wa-space-l) 0;"></wa-divider>
<ul class="sidebar-nav">
<li>
<a href="connections.html">
<wa-icon name="plug"></wa-icon>
Connections
</a>
</li>
<li>
<a href="device.html">
<wa-icon name="mobile"></wa-icon>
Devices
</a>
</li>
<li>
<a href="service.html" class="active">
<wa-icon name="server"></wa-icon>
Services
</a>
</li>
<li>
<a href="settings.html">
<wa-icon name="gear"></wa-icon>
Settings
</a>
</li>
</ul>
</aside>
<main class="main-content">
<a href="connections.html" class="back-link">
<wa-icon name="arrow-left"></wa-icon>
Back to Connections
</a>
<!-- Service Hero -->
<div class="service-hero">
<div class="service-identity">
<div class="service-logo" id="service-logo">
<wa-icon name="arrow-right-arrow-left"></wa-icon>
</div>
<div class="service-name-block">
<div class="wa-cluster wa-gap-s wa-align-items-center">
<span class="wa-heading-2xl" id="service-name">Uniswap</span>
</div>
<span class="wa-caption-m" style="color: var(--wa-color-neutral-500);" id="service-domain">app.uniswap.org</span>
<div class="service-badges">
<wa-badge variant="success" pill id="verified-badge">
<wa-icon name="badge-check"></wa-icon>
Verified
</wa-badge>
<wa-badge variant="primary" pill id="status-badge">
<wa-icon name="circle" variant="solid" style="font-size: 6px;"></wa-icon>
Active
</wa-badge>
<wa-badge variant="neutral" pill>
<wa-icon name="coins"></wa-icon>
DeFi
</wa-badge>
</div>
<p class="service-description">
Uniswap is a decentralized exchange protocol that allows users to swap ERC-20 tokens without intermediaries. Trade, earn, and build on the leading decentralized crypto trading protocol.
</p>
</div>
</div>
<div class="status-block">
<wa-badge variant="success" size="large">Connected</wa-badge>
<div class="connection-info">
<span>Connected: <strong>Dec 20, 2024</strong></span>
<span>Last used: <strong>2 hours ago</strong></span>
<span>Expires: <strong>Never</strong></span>
</div>
</div>
<div class="hero-actions">
<wa-button variant="neutral" appearance="outlined" id="visit-site-btn" onclick="window.open('https://app.uniswap.org', '_blank')">
<wa-icon slot="start" name="arrow-up-right-from-square"></wa-icon>
Visit Site
</wa-button>
<wa-button variant="neutral" appearance="outlined" id="refresh-token-btn">
<wa-icon slot="start" name="rotate"></wa-icon>
Refresh Token
</wa-button>
<div style="flex: 1;"></div>
<wa-button variant="danger" appearance="outlined" id="disconnect-btn">
<wa-icon slot="start" name="plug-circle-xmark"></wa-icon>
Disconnect
</wa-button>
</div>
</div>
<!-- Stats Grid -->
<div class="stats-grid">
<div class="stat-item">
<div class="stat-icon info">
<wa-icon name="clock"></wa-icon>
</div>
<div class="stat-label">Active Sessions</div>
<div class="stat-value">3</div>
</div>
<div class="stat-item">
<div class="stat-icon warning">
<wa-icon name="signature"></wa-icon>
</div>
<div class="stat-label">Signatures (30d)</div>
<div class="stat-value">47</div>
<div class="stat-trend up">
<wa-icon name="arrow-trend-up" style="font-size: 10px;"></wa-icon>
+12% vs last month
</div>
</div>
<div class="stat-item">
<div class="stat-icon success">
<wa-icon name="check"></wa-icon>
</div>
<div class="stat-label">TX Approved</div>
<div class="stat-value">89</div>
</div>
<div class="stat-item">
<div class="stat-icon danger">
<wa-icon name="xmark"></wa-icon>
</div>
<div class="stat-label">TX Rejected</div>
<div class="stat-value">3</div>
</div>
<div class="stat-item">
<div class="stat-icon neutral">
<wa-icon name="shield-check"></wa-icon>
</div>
<div class="stat-label">Permissions</div>
<div class="stat-value">4 / 6</div>
</div>
<div class="stat-item">
<div class="stat-icon info">
<wa-icon name="database"></wa-icon>
</div>
<div class="stat-label">Data Requests</div>
<div class="stat-value">234</div>
</div>
</div>
<!-- Permissions & OAuth Info Grid -->
<div class="content-grid">
<!-- Permissions Card -->
<wa-card>
<div slot="header" class="wa-flank">
<span class="wa-heading-m">Permissions</span>
<wa-button appearance="plain" size="small" id="reset-permissions-btn">Reset to Default</wa-button>
</div>
<ul class="permissions-list" id="permissions-list">
<li class="permission-item">
<div class="permission-info">
<div class="permission-header">
<span class="permission-name">View Profile</span>
<wa-badge variant="neutral" size="small">Required</wa-badge>
</div>
<span class="permission-scope">openid profile</span>
<p class="permission-description">Access your public profile information including display name and avatar.</p>
<div class="permission-usage">Used 45 times · Last used 2 hours ago</div>
</div>
<wa-switch checked disabled></wa-switch>
</li>
<li class="permission-item">
<div class="permission-info">
<div class="permission-header">
<span class="permission-name">View Wallet Balance</span>
<wa-badge variant="neutral" size="small">Required</wa-badge>
</div>
<span class="permission-scope">wallet:read</span>
<p class="permission-description">View your wallet balances and transaction history.</p>
<div class="permission-usage">Used 189 times · Last used 2 hours ago</div>
</div>
<wa-switch checked disabled></wa-switch>
</li>
<li class="permission-item">
<div class="permission-info">
<div class="permission-header">
<span class="permission-name">Request Signatures</span>
<wa-badge variant="primary" size="small">Optional</wa-badge>
</div>
<span class="permission-scope">wallet:sign</span>
<p class="permission-description">Request message signatures for authentication and verification.</p>
<div class="permission-usage">Used 47 times · Last used 5 hours ago</div>
</div>
<wa-switch checked id="perm-sign"></wa-switch>
</li>
<li class="permission-item">
<div class="permission-info">
<div class="permission-header">
<span class="permission-name">Submit Transactions</span>
<wa-badge variant="primary" size="small">Optional</wa-badge>
</div>
<span class="permission-scope">wallet:transact</span>
<p class="permission-description">Request approval to submit transactions on your behalf. Each transaction requires explicit approval.</p>
<div class="permission-usage">Used 92 times · Last used 1 day ago</div>
</div>
<wa-switch checked id="perm-transact"></wa-switch>
</li>
<li class="permission-item">
<div class="permission-info">
<div class="permission-header">
<span class="permission-name">View NFTs</span>
<wa-badge variant="primary" size="small">Optional</wa-badge>
</div>
<span class="permission-scope">wallet:nfts:read</span>
<p class="permission-description">View your NFT collection and metadata.</p>
<div class="permission-usage">Never used</div>
</div>
<wa-switch id="perm-nfts"></wa-switch>
</li>
<li class="permission-item">
<div class="permission-info">
<div class="permission-header">
<span class="permission-name">Notifications</span>
<wa-badge variant="primary" size="small">Optional</wa-badge>
</div>
<span class="permission-scope">notifications</span>
<p class="permission-description">Send you notifications about swaps, price alerts, and governance proposals.</p>
<div class="permission-usage">Never used</div>
</div>
<wa-switch id="perm-notifications"></wa-switch>
</li>
</ul>
<div slot="footer" id="permission-status"></div>
</wa-card>
<!-- OAuth Info Card -->
<wa-card>
<div slot="header">
<span class="wa-heading-m">Connection Details</span>
</div>
<div class="oauth-display">
<span class="oauth-label">Client ID</span>
<div class="oauth-value" id="client-id">uniswap_0x7a2b4c9d8e3f1a5b6c7d8e9f0a1b2c3d</div>
</div>
<div class="oauth-grid">
<div class="oauth-item">
<span class="oauth-item-label">Token Type</span>
<span class="oauth-item-value">Bearer</span>
</div>
<div class="oauth-item">
<span class="oauth-item-label">Issued At</span>
<span class="oauth-item-value">Dec 20, 2024</span>
</div>
<div class="oauth-item">
<span class="oauth-item-label">Expires</span>
<span class="oauth-item-value">Never (Refresh Token)</span>
</div>
<div class="oauth-item">
<span class="oauth-item-label">Granted Scopes</span>
<span class="oauth-item-value">4 of 6</span>
</div>
</div>
<wa-divider style="margin: var(--wa-space-m) 0;"></wa-divider>
<div class="wa-stack wa-gap-s">
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Redirect URIs</span>
<ul class="redirect-uris">
<li>https://app.uniswap.org/callback</li>
<li>https://app.uniswap.org/auth/callback</li>
</ul>
</div>
<wa-divider style="margin: var(--wa-space-m) 0;"></wa-divider>
<div class="resource-links">
<a href="https://uniswap.org" target="_blank" class="resource-link">
<wa-icon name="globe"></wa-icon>
Website
</a>
<a href="https://uniswap.org/privacy-policy" target="_blank" class="resource-link">
<wa-icon name="shield-halved"></wa-icon>
Privacy Policy
</a>
<a href="https://uniswap.org/terms-of-service" target="_blank" class="resource-link">
<wa-icon name="file-contract"></wa-icon>
Terms of Service
</a>
<a href="mailto:support@uniswap.org" class="resource-link">
<wa-icon name="envelope"></wa-icon>
Support
</a>
</div>
</wa-card>
</div>
<!-- Session Log -->
<wa-card style="margin-bottom: var(--wa-space-xl);">
<div slot="header" class="wa-flank">
<span class="wa-heading-m">Activity Log</span>
<div class="wa-cluster wa-gap-s">
<wa-button appearance="plain" size="small" id="refresh-log-btn">
<wa-icon name="rotate"></wa-icon>
</wa-button>
<wa-button appearance="plain" size="small">Export</wa-button>
</div>
</div>
<wa-radio-group value="all" orientation="horizontal" class="filter-tabs" id="log-filter">
<wa-radio appearance="button" value="all">All</wa-radio>
<wa-radio appearance="button" value="auth">Auth</wa-radio>
<wa-radio appearance="button" value="signatures">Signatures</wa-radio>
<wa-radio appearance="button" value="transactions">Transactions</wa-radio>
</wa-radio-group>
<ul class="session-log-list" id="session-log">
<li class="session-item">
<div class="session-icon auth">
<wa-icon name="key"></wa-icon>
</div>
<div class="session-content">
<div class="session-title">Session started</div>
<div class="session-meta">2 hours ago · 192.168.1.42 · San Francisco, CA</div>
<div class="session-details">Chrome 120 on macOS</div>
</div>
<wa-badge variant="success" size="small">Active</wa-badge>
</li>
<li class="session-item">
<div class="session-icon sign">
<wa-icon name="signature"></wa-icon>
</div>
<div class="session-content">
<div class="session-title">Message signed</div>
<div class="session-meta">2 hours ago · 192.168.1.42</div>
<div class="session-details">Signed login message for Uniswap</div>
</div>
<wa-badge variant="success" size="small">Success</wa-badge>
</li>
<li class="session-item">
<div class="session-icon transaction">
<wa-icon name="arrow-right-arrow-left"></wa-icon>
</div>
<div class="session-content">
<div class="session-title">Swap executed</div>
<div class="session-meta">5 hours ago · 192.168.1.42</div>
<div class="session-details">
Swapped 0.5 ETH → 1,245.32 USDC
<br>
<a href="#" class="session-tx-hash">0x7a2b4c9d...8e3f1a5b</a>
</div>
</div>
<wa-badge variant="success" size="small">Confirmed</wa-badge>
</li>
<li class="session-item">
<div class="session-icon data">
<wa-icon name="database"></wa-icon>
</div>
<div class="session-content">
<div class="session-title">Balance checked</div>
<div class="session-meta">5 hours ago · 192.168.1.42</div>
<div class="session-details">Read wallet balances for swap interface</div>
</div>
</li>
<li class="session-item">
<div class="session-icon transaction">
<wa-icon name="arrow-right-arrow-left"></wa-icon>
</div>
<div class="session-content">
<div class="session-title">Swap executed</div>
<div class="session-meta">1 day ago · 192.168.1.42</div>
<div class="session-details">
Swapped 100 USDC → 0.042 ETH
<br>
<a href="#" class="session-tx-hash">0x9c8d7e6f...2a1b0c9d</a>
</div>
</div>
<wa-badge variant="success" size="small">Confirmed</wa-badge>
</li>
<li class="session-item">
<div class="session-icon error">
<wa-icon name="xmark"></wa-icon>
</div>
<div class="session-content">
<div class="session-title">Transaction rejected</div>
<div class="session-meta">2 days ago · 192.168.1.42</div>
<div class="session-details">User declined swap request: 1 ETH → USDT</div>
</div>
<wa-badge variant="danger" size="small">Rejected</wa-badge>
</li>
<li class="session-item">
<div class="session-icon sign">
<wa-icon name="signature"></wa-icon>
</div>
<div class="session-content">
<div class="session-title">Permit signature</div>
<div class="session-meta">3 days ago · 192.168.1.42</div>
<div class="session-details">Signed EIP-2612 permit for USDC spending</div>
</div>
<wa-badge variant="success" size="small">Success</wa-badge>
</li>
<li class="session-item">
<div class="session-icon auth">
<wa-icon name="plug"></wa-icon>
</div>
<div class="session-content">
<div class="session-title">App connected</div>
<div class="session-meta">Dec 20, 2024 · 192.168.1.42 · San Francisco, CA</div>
<div class="session-details">Initial connection with 4 permissions granted</div>
</div>
<wa-badge variant="primary" size="small">Connected</wa-badge>
</li>
</ul>
<div slot="footer">
<a href="#" class="wa-cluster wa-gap-xs wa-caption-s" style="color: var(--wa-color-primary);">
<span>View full history</span>
<wa-icon name="arrow-right"></wa-icon>
</a>
</div>
</wa-card>
<!-- Data Access Summary -->
<wa-card>
<div slot="header">
<span class="wa-heading-m">Data Access Summary</span>
</div>
<div class="data-access-grid">
<div class="data-access-item">
<div class="data-access-count">45</div>
<div class="data-access-label">Profile Reads</div>
</div>
<div class="data-access-item">
<div class="data-access-count">189</div>
<div class="data-access-label">Balance Checks</div>
</div>
<div class="data-access-item">
<div class="data-access-count">56</div>
<div class="data-access-label">Transaction Reads</div>
</div>
<div class="data-access-item">
<div class="data-access-count">0</div>
<div class="data-access-label">NFT Reads</div>
</div>
<div class="data-access-item">
<div class="data-access-count">47</div>
<div class="data-access-label">Signature Requests</div>
</div>
<div class="data-access-item">
<div class="data-access-count">92</div>
<div class="data-access-label">TX Requests</div>
</div>
</div>
<wa-divider style="margin: var(--wa-space-m) 0;"></wa-divider>
<div class="wa-cluster wa-gap-m wa-caption-s" style="color: var(--wa-color-neutral-500);">
<span>Last data request: <strong>2 hours ago</strong></span>
<span>Total requests: <strong>429</strong></span>
</div>
</wa-card>
</main>
</div>
</wa-page>
<!-- Disconnect Confirmation Dialog -->
<wa-dialog id="disconnect-dialog" label="Disconnect Uniswap">
<div class="wa-stack wa-gap-m">
<wa-callout variant="warning">
<wa-icon slot="icon" name="triangle-exclamation"></wa-icon>
<strong>This will revoke all access</strong>
<p style="margin: var(--wa-space-xs) 0 0 0;">
Uniswap will no longer be able to view your wallet or request signatures and transactions.
</p>
</wa-callout>
<p class="wa-caption-s">
You can reconnect at any time by visiting Uniswap and authorizing again. Your transaction history will be preserved.
</p>
</div>
<div slot="footer" class="wa-cluster wa-gap-s wa-justify-content-end">
<wa-button variant="neutral" appearance="outlined" id="disconnect-cancel-btn">Cancel</wa-button>
<wa-button variant="danger" id="disconnect-confirm-btn">Disconnect</wa-button>
</div>
</wa-dialog>
<!-- Reset Permissions Dialog -->
<wa-dialog id="reset-dialog" label="Reset Permissions">
<div class="wa-stack wa-gap-m">
<p class="wa-caption-s">
This will revoke all optional permissions and keep only the required ones (profile and wallet:read).
</p>
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500);">
The following permissions will be revoked:
</p>
<ul style="margin: 0; padding-left: var(--wa-space-l); font-size: var(--wa-font-size-xs); color: var(--wa-color-neutral-600);">
<li>Request Signatures (wallet:sign)</li>
<li>Submit Transactions (wallet:transact)</li>
</ul>
</div>
<div slot="footer" class="wa-cluster wa-gap-s wa-justify-content-end">
<wa-button variant="neutral" appearance="outlined" id="reset-cancel-btn">Cancel</wa-button>
<wa-button variant="warning" id="reset-confirm-btn">Reset Permissions</wa-button>
</div>
</wa-dialog>
<script>
// Disconnect dialog handlers
const disconnectDialog = document.getElementById('disconnect-dialog');
const disconnectBtn = document.getElementById('disconnect-btn');
const disconnectCancelBtn = document.getElementById('disconnect-cancel-btn');
const disconnectConfirmBtn = document.getElementById('disconnect-confirm-btn');
disconnectBtn?.addEventListener('click', () => {
disconnectDialog.open = true;
});
disconnectCancelBtn?.addEventListener('click', () => {
disconnectDialog.open = false;
});
disconnectConfirmBtn?.addEventListener('click', () => {
window.location.href = 'connections.html';
});
// Reset permissions dialog handlers
const resetDialog = document.getElementById('reset-dialog');
const resetBtn = document.getElementById('reset-permissions-btn');
const resetCancelBtn = document.getElementById('reset-cancel-btn');
const resetConfirmBtn = document.getElementById('reset-confirm-btn');
resetBtn?.addEventListener('click', () => {
resetDialog.open = true;
});
resetCancelBtn?.addEventListener('click', () => {
resetDialog.open = false;
});
resetConfirmBtn?.addEventListener('click', () => {
document.getElementById('perm-sign').checked = false;
document.getElementById('perm-transact').checked = false;
document.getElementById('perm-nfts').checked = false;
document.getElementById('perm-notifications').checked = false;
resetDialog.open = false;
});
// Permission toggles
document.querySelectorAll('.permission-item wa-switch:not([disabled])').forEach(toggle => {
toggle.addEventListener('wa-change', function() {
const status = document.getElementById('permission-status');
status.innerHTML = `
<wa-callout variant="success" style="margin-top: var(--wa-space-s);">
<wa-icon slot="icon" name="check"></wa-icon>
Permission ${this.checked ? 'granted' : 'revoked'} successfully
</wa-callout>
`;
setTimeout(() => {
status.innerHTML = '';
}, 3000);
});
});
// Refresh token handler
const refreshTokenBtn = document.getElementById('refresh-token-btn');
refreshTokenBtn?.addEventListener('click', function() {
const icon = this.querySelector('wa-icon');
icon.style.animation = 'spin 0.5s linear';
setTimeout(() => {
icon.style.animation = '';
}, 500);
});
// Refresh log handler
const refreshLogBtn = document.getElementById('refresh-log-btn');
refreshLogBtn?.addEventListener('click', function() {
const icon = this.querySelector('wa-icon');
icon.style.animation = 'spin 0.5s linear';
setTimeout(() => {
icon.style.animation = '';
}, 500);
});
// Log filter
document.getElementById('log-filter')?.addEventListener('wa-change', function(e) {
console.log('Filter changed to:', e.target.value);
});
// Sidebar navigation
document.querySelectorAll('.sidebar-nav a').forEach(link => {
link.addEventListener('click', function(e) {
if (this.getAttribute('href') === '#') {
e.preventDefault();
}
});
});
// Add spin animation
const style = document.createElement('style');
style.textContent = `
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
</script>
</body>
</html>