feat(identity): add My Wallets panel to avatar dropdown
Adds a wallet modal accessible from the identity dropdown showing: - rIdentity wallet card with username, DID, and passkey badge - Browser wallet discovery via EIP-6963 (MetaMask, Rainbow, etc.) - Connect flow with eth_requestAccounts - Quick link to open the full rWallet module Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c36b0abc32
commit
e47cd35a34
|
|
@ -6,7 +6,7 @@
|
|||
* Refactored from lib/rspace-header.ts into a standalone web component.
|
||||
*/
|
||||
|
||||
import { rspaceNavUrl, getCurrentModule } from "../url-helpers";
|
||||
import { rspaceNavUrl, getCurrentModule, getCurrentSpace } from "../url-helpers";
|
||||
import { resetDocBridge, isEncryptedBackupEnabled, setEncryptedBackupEnabled } from "../local-first/encryptid-bridge";
|
||||
|
||||
const SESSION_KEY = "encryptid_session";
|
||||
|
|
@ -28,6 +28,32 @@ interface SessionState {
|
|||
};
|
||||
}
|
||||
|
||||
// ── EIP-6963 browser wallet discovery ──
|
||||
|
||||
interface _EIP6963Provider {
|
||||
info: { uuid: string; name: string; icon: string; rdns: string };
|
||||
provider: { request: (args: { method: string; params?: unknown[] }) => Promise<unknown> };
|
||||
}
|
||||
|
||||
class _WalletDiscovery {
|
||||
providers: _EIP6963Provider[] = [];
|
||||
#cb: (() => void) | null = null;
|
||||
|
||||
start(onChange: () => void) {
|
||||
this.#cb = onChange;
|
||||
window.addEventListener("eip6963:announceProvider", ((e: CustomEvent) => {
|
||||
const detail = e.detail as _EIP6963Provider;
|
||||
if (!this.providers.some((p) => p.info.uuid === detail.info.uuid)) {
|
||||
this.providers.push(detail);
|
||||
this.#cb?.();
|
||||
}
|
||||
}) as EventListener);
|
||||
window.dispatchEvent(new Event("eip6963:requestProvider"));
|
||||
}
|
||||
|
||||
stop() { this.#cb = null; }
|
||||
}
|
||||
|
||||
// ── Cross-subdomain cookie helpers ──
|
||||
|
||||
function _isRspace(): boolean {
|
||||
|
|
@ -353,6 +379,7 @@ export class RStackIdentity extends HTMLElement {
|
|||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item" data-action="my-account">👤 My Account</button>
|
||||
<button class="dropdown-item" data-action="my-spaces">🌐 My Spaces</button>
|
||||
<button class="dropdown-item" data-action="my-wallets">💰 My Wallets</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="dropdown-theme-row">
|
||||
<span class="theme-icon">☀️</span>
|
||||
|
|
@ -400,6 +427,8 @@ export class RStackIdentity extends HTMLElement {
|
|||
this.#showAccountModal();
|
||||
} else if (action === "my-spaces") {
|
||||
this.#showSpacesModal();
|
||||
} else if (action === "my-wallets") {
|
||||
this.#showWalletsModal();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1202,6 +1231,103 @@ export class RStackIdentity extends HTMLElement {
|
|||
render();
|
||||
}
|
||||
|
||||
// ── Wallets modal ──
|
||||
|
||||
#showWalletsModal(): void {
|
||||
if (document.querySelector(".rstack-wallets-overlay")) return;
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "rstack-wallets-overlay";
|
||||
|
||||
const session = getSession();
|
||||
const username = session?.claims.username || "";
|
||||
const did = session?.claims.did || session?.claims.sub || "";
|
||||
const initial = username ? username[0].toUpperCase() : did.slice(8, 10).toUpperCase();
|
||||
const truncDid = did.length > 32 ? did.slice(0, 16) + "..." + did.slice(-8) : did;
|
||||
|
||||
const discovery = new _WalletDiscovery();
|
||||
const connectedAddrs = new Map<string, string>(); // uuid → address
|
||||
|
||||
const close = () => { discovery.stop(); overlay.remove(); };
|
||||
|
||||
const render = () => {
|
||||
const browserSection = discovery.providers.length
|
||||
? discovery.providers.map((p) => {
|
||||
const addr = connectedAddrs.get(p.info.uuid);
|
||||
return `
|
||||
<div class="wallet-provider-card" data-uuid="${p.info.uuid}">
|
||||
<img class="wallet-provider-icon" src="${p.info.icon}" alt="${p.info.name}" />
|
||||
<div class="wallet-provider-info">
|
||||
<div class="wallet-provider-name">${p.info.name}</div>
|
||||
${addr
|
||||
? `<div class="wallet-provider-addr">${addr.slice(0, 6)}...${addr.slice(-4)}</div>`
|
||||
: `<button class="wallet-connect-btn" data-connect="${p.info.uuid}">Connect</button>`
|
||||
}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join("")
|
||||
: `<p class="wallet-empty">No browser wallets detected.<br><span style="font-size:0.75rem;color:var(--rs-text-muted)">Install MetaMask, Rainbow, or another EIP-6963 wallet.</span></p>`;
|
||||
|
||||
overlay.innerHTML = `
|
||||
<style>${MODAL_STYLES}${WALLETS_STYLES}</style>
|
||||
<div class="wallets-modal">
|
||||
<button class="close-btn" data-action="close">×</button>
|
||||
<h2>My Wallets</h2>
|
||||
|
||||
<div class="wallet-section-label">rIdentity Wallet</div>
|
||||
<div class="wallet-identity-card">
|
||||
<div class="wallet-identity-avatar">${initial}</div>
|
||||
<div class="wallet-identity-info">
|
||||
<div class="wallet-identity-name">${username || "Anonymous"}</div>
|
||||
<div class="wallet-identity-did" title="${did}">${truncDid}</div>
|
||||
</div>
|
||||
<span class="wallet-badge">Passkey</span>
|
||||
</div>
|
||||
|
||||
<div class="wallet-section-label">Browser Wallets</div>
|
||||
<div class="wallet-providers">${browserSection}</div>
|
||||
|
||||
<div class="wallet-footer">
|
||||
<button class="wallet-open-btn" data-action="open-rwallet">Open rWallet →</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
overlay.querySelector('[data-action="close"]')?.addEventListener("click", close);
|
||||
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
|
||||
|
||||
overlay.querySelector('[data-action="open-rwallet"]')?.addEventListener("click", () => {
|
||||
close();
|
||||
const space = _getCurrentSpace();
|
||||
window.location.href = _navUrl(space, "rwallet");
|
||||
});
|
||||
|
||||
overlay.querySelectorAll("[data-connect]").forEach((btn) => {
|
||||
btn.addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
const uuid = (btn as HTMLElement).dataset.connect!;
|
||||
const provider = discovery.providers.find((p) => p.info.uuid === uuid);
|
||||
if (!provider) return;
|
||||
(btn as HTMLButtonElement).textContent = "Connecting...";
|
||||
(btn as HTMLButtonElement).disabled = true;
|
||||
try {
|
||||
const accounts = await provider.provider.request({ method: "eth_requestAccounts" }) as string[];
|
||||
if (accounts?.[0]) {
|
||||
connectedAddrs.set(uuid, accounts[0]);
|
||||
render();
|
||||
}
|
||||
} catch {
|
||||
(btn as HTMLButtonElement).textContent = "Rejected";
|
||||
setTimeout(() => { (btn as HTMLButtonElement).textContent = "Connect"; (btn as HTMLButtonElement).disabled = false; }, 2000);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
discovery.start(render);
|
||||
render();
|
||||
}
|
||||
|
||||
// ── Spaces modal ──
|
||||
|
||||
#showSpacesModal(): void {
|
||||
|
|
@ -1759,3 +1885,91 @@ const SPACES_STYLES = `
|
|||
background: linear-gradient(135deg, #06b6d4, #7c3aed);
|
||||
}
|
||||
`;
|
||||
|
||||
const WALLETS_STYLES = `
|
||||
.rstack-wallets-overlay {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,0.6);
|
||||
backdrop-filter: blur(4px); display: flex; align-items: center;
|
||||
justify-content: center; z-index: 10000; animation: fadeIn 0.2s;
|
||||
}
|
||||
.wallets-modal {
|
||||
background: var(--rs-bg-surface); border: 1px solid var(--rs-border);
|
||||
border-radius: 16px; padding: 2rem; max-width: 480px; width: 92%;
|
||||
max-height: 85vh; overflow-y: auto; color: var(--rs-text-primary); position: relative;
|
||||
box-shadow: var(--rs-shadow-lg); animation: slideUp 0.3s;
|
||||
}
|
||||
.wallets-modal h2 {
|
||||
font-size: 1.5rem; margin-bottom: 0.5rem;
|
||||
background: linear-gradient(135deg, #06b6d4, #7c3aed);
|
||||
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
||||
}
|
||||
.wallet-section-label {
|
||||
font-size: 0.75rem; font-weight: 600; text-transform: uppercase;
|
||||
letter-spacing: 0.05em; color: var(--rs-text-muted); margin: 1.25rem 0 0.5rem;
|
||||
}
|
||||
.wallet-identity-card {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 14px 16px; border-radius: 12px;
|
||||
background: var(--rs-bg-hover); border: 1px solid rgba(6,182,212,0.3);
|
||||
}
|
||||
.wallet-identity-avatar {
|
||||
width: 42px; height: 42px; border-radius: 50%; flex-shrink: 0;
|
||||
background: linear-gradient(135deg, #06b6d4, #7c3aed);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 700; font-size: 0.9rem; color: white;
|
||||
}
|
||||
.wallet-identity-info { flex: 1; min-width: 0; }
|
||||
.wallet-identity-name {
|
||||
font-weight: 600; font-size: 0.95rem; color: var(--rs-text-primary);
|
||||
}
|
||||
.wallet-identity-did {
|
||||
font-size: 0.75rem; color: var(--rs-text-muted); font-family: monospace;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.wallet-badge {
|
||||
font-size: 0.7rem; font-weight: 600; color: #06b6d4;
|
||||
background: rgba(6,182,212,0.12); padding: 3px 10px; border-radius: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.wallet-providers { display: flex; flex-direction: column; gap: 8px; }
|
||||
.wallet-provider-card {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 12px 14px; border-radius: 10px;
|
||||
background: var(--rs-bg-hover); border: 1px solid var(--rs-border);
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.wallet-provider-card:hover { border-color: rgba(6,182,212,0.4); }
|
||||
.wallet-provider-icon {
|
||||
width: 32px; height: 32px; border-radius: 8px; flex-shrink: 0;
|
||||
}
|
||||
.wallet-provider-info { flex: 1; min-width: 0; }
|
||||
.wallet-provider-name {
|
||||
font-weight: 600; font-size: 0.875rem; color: var(--rs-text-primary);
|
||||
}
|
||||
.wallet-provider-addr {
|
||||
font-size: 0.75rem; color: #34d399; font-family: monospace;
|
||||
}
|
||||
.wallet-connect-btn {
|
||||
font-size: 0.75rem; font-weight: 600; padding: 4px 14px;
|
||||
border-radius: 8px; border: 1px solid rgba(6,182,212,0.4);
|
||||
background: rgba(6,182,212,0.1); color: #06b6d4; cursor: pointer;
|
||||
transition: all 0.15s; font-family: inherit;
|
||||
}
|
||||
.wallet-connect-btn:hover { background: rgba(6,182,212,0.2); }
|
||||
.wallet-connect-btn:disabled { opacity: 0.6; cursor: default; }
|
||||
.wallet-empty {
|
||||
text-align: center; padding: 1rem 0;
|
||||
color: var(--rs-text-secondary); font-size: 0.85rem; line-height: 1.6;
|
||||
}
|
||||
.wallet-footer {
|
||||
margin-top: 1.5rem; padding-top: 1rem;
|
||||
border-top: 1px solid var(--rs-border); text-align: center;
|
||||
}
|
||||
.wallet-open-btn {
|
||||
font-size: 0.875rem; font-weight: 600; padding: 8px 24px;
|
||||
border-radius: 10px; border: none; cursor: pointer;
|
||||
background: linear-gradient(135deg, #06b6d4, #7c3aed); color: white;
|
||||
transition: all 0.2s; font-family: inherit;
|
||||
}
|
||||
.wallet-open-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(6,182,212,0.3); }
|
||||
`;
|
||||
|
|
|
|||
Loading…
Reference in New Issue