From a62a33b4dc4d0192f5e1190b85e1d0ba20530a6e Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Fri, 6 Mar 2026 23:57:55 -0800 Subject: [PATCH] feat: add wallet and spaces sections to EncryptID account page - My Wallet: shows linked wallet address with rWallet link, plus any pending fund claims with "Claim Now" button - My Spaces: lists communities the user belongs to with role and treasury wallet links - New APIs: GET /api/user/spaces, GET /api/user/claims - New DB function: listSpacesForUser() Co-Authored-By: Claude Opus 4.6 --- src/encryptid/db.ts | 15 +++++ src/encryptid/server.ts | 140 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 152 insertions(+), 3 deletions(-) diff --git a/src/encryptid/db.ts b/src/encryptid/db.ts index a9664d6..0b93baf 100644 --- a/src/encryptid/db.ts +++ b/src/encryptid/db.ts @@ -274,6 +274,21 @@ export interface StoredSpaceMember { grantedBy?: string; } +export async function listSpacesForUser(userDID: string): Promise { + const rows = await sql` + SELECT * FROM space_members + WHERE user_did = ${userDID} + ORDER BY joined_at DESC + `; + return rows.map((row) => ({ + spaceSlug: row.space_slug, + userDID: row.user_did, + role: row.role, + joinedAt: new Date(row.joined_at).getTime(), + grantedBy: row.granted_by || undefined, + })); +} + export async function getSpaceMember( spaceSlug: string, userDID: string, diff --git a/src/encryptid/server.ts b/src/encryptid/server.ts index c9cad71..f495586 100644 --- a/src/encryptid/server.ts +++ b/src/encryptid/server.ts @@ -55,6 +55,7 @@ import { markDeviceLinkUsed, getSpaceMember, listSpaceMembers, + listSpacesForUser, upsertSpaceMember, removeSpaceMember, getUserProfile, @@ -753,6 +754,38 @@ app.get('/api/user/profile', async (c) => { return c.json({ success: true, profile }); }); +// GET /api/user/spaces — list spaces the user is a member of +app.get('/api/user/spaces', async (c) => { + const claims = await verifyTokenFromRequest(c.req.header('Authorization')); + if (!claims) return c.json({ error: 'Unauthorized' }, 401); + + const spaces = await listSpacesForUser(claims.sub); + return c.json({ spaces }); +}); + +// GET /api/user/claims — list pending fund claims for the authenticated user +app.get('/api/user/claims', async (c) => { + const claims = await verifyTokenFromRequest(c.req.header('Authorization')); + if (!claims) return c.json({ error: 'Unauthorized' }, 401); + + const profile = await getUserProfile(claims.sub); + if (!profile?.profileEmail) return c.json({ claims: [] }); + + const emailHashed = await hashEmail(profile.profileEmail); + const pendingClaims = await getFundClaimsByEmailHash(emailHashed); + return c.json({ + claims: pendingClaims.map(cl => ({ + id: cl.id, + token: cl.token, + fiatAmount: cl.fiatAmount, + fiatCurrency: cl.fiatCurrency, + walletAddress: cl.walletAddress, + expiresAt: cl.expiresAt, + status: cl.status, + })), + }); +}); + // PUT /api/user/profile — update the authenticated user's profile app.put('/api/user/profile', async (c) => { const claims = await verifyTokenFromRequest(c.req.header('Authorization')); @@ -3344,8 +3377,22 @@ app.get('/', (c) => { .passkey-date { color: #64748b; } /* Recovery email */ - .recovery-section, .guardians-section, .devices-section, .vault-section { margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid rgba(255,255,255,0.08); } - .recovery-section h4, .guardians-section h4, .devices-section h4, .vault-section h4 { font-size: 0.9rem; margin-bottom: 0.75rem; color: #94a3b8; } + .recovery-section, .guardians-section, .devices-section, .vault-section, .wallet-section, .spaces-section { margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid rgba(255,255,255,0.08); } + .recovery-section h4, .guardians-section h4, .devices-section h4, .vault-section h4, .wallet-section h4, .spaces-section h4 { font-size: 0.9rem; margin-bottom: 0.75rem; color: #94a3b8; } + .wallet-card { padding: 0.75rem; border-radius: 0.5rem; background: rgba(255,255,255,0.04); margin-bottom: 0.5rem; } + .wallet-card .wallet-label { font-size: 0.75rem; color: #64748b; text-transform: uppercase; margin-bottom: 0.25rem; } + .wallet-card .wallet-addr { font-family: monospace; font-size: 0.85rem; color: #e2e8f0; word-break: break-all; } + .wallet-card .wallet-addr a { color: #60a5fa; text-decoration: none; } + .wallet-card .wallet-addr a:hover { text-decoration: underline; } + .wallet-card .wallet-balance { font-size: 0.8rem; color: #4ade80; margin-top: 0.25rem; } + .space-card { padding: 0.75rem; border-radius: 0.5rem; background: rgba(255,255,255,0.04); margin-bottom: 0.5rem; display: flex; justify-content: space-between; align-items: center; } + .space-card .space-name { font-weight: 600; color: #e2e8f0; } + .space-card .space-name a { color: #e2e8f0; text-decoration: none; } + .space-card .space-name a:hover { color: #60a5fa; } + .space-card .space-role { font-size: 0.75rem; color: #64748b; text-transform: uppercase; } + .space-wallet { font-size: 0.8rem; color: #94a3b8; margin-top: 0.25rem; font-family: monospace; } + .space-wallet a { color: #60a5fa; text-decoration: none; } + .space-wallet a:hover { text-decoration: underline; } .recovery-row { display: flex; gap: 0.5rem; } .recovery-row input { flex: 1; } .recovery-row button { white-space: nowrap; } @@ -3538,6 +3585,19 @@ app.get('/', (c) => { +
+

My Wallet

+

Your EncryptID-linked wallet for receiving and managing funds.

+
No wallet linked
+
+
+ +
+

My Spaces

+

Communities you belong to and their treasury wallets.

+
Loading...
+
+
SDK Demo @@ -3803,9 +3863,83 @@ app.get('/', (c) => { } } catch { /* ignore */ } - // Load guardians and check vault status + // Load guardians, vault status, wallet, and spaces loadGuardians(); checkVaultStatus(); + loadWalletInfo(token, username); + loadUserSpaces(token); + } + + async function loadWalletInfo(token, username) { + var walletEl = document.getElementById('wallet-info'); + var claimsEl = document.getElementById('wallet-claims'); + try { + var res = await fetch('/api/user/profile', { + headers: { 'Authorization': 'Bearer ' + token }, + }); + var data = await res.json(); + if (data.profile && data.profile.walletAddress) { + var addr = data.profile.walletAddress; + var short = addr.slice(0, 6) + '...' + addr.slice(-4); + var rwalletUrl = 'https://' + (username || 'demo') + '.rspace.online/rwallet?address=' + encodeURIComponent(addr); + walletEl.innerHTML = + '
' + + '
Linked Wallet
' + + '' + + '
'; + } else { + walletEl.innerHTML = '
No wallet linked yet. Complete a fund claim or add one manually.
'; + } + + // Check for pending fund claims + var claimRes = await fetch('/api/user/claims', { + headers: { 'Authorization': 'Bearer ' + token }, + }); + if (claimRes.ok) { + var claimData = await claimRes.json(); + if (claimData.claims && claimData.claims.length > 0) { + claimsEl.innerHTML = claimData.claims.map(function(cl) { + return '
' + + '
Pending Claim
' + + '
$' + (cl.fiatAmount || '0') + ' ' + (cl.fiatCurrency || 'USD') + '
' + + '
Expires ' + new Date(cl.expiresAt).toLocaleDateString() + '
' + + 'Claim Now' + + '
'; + }).join(''); + } + } + } catch (err) { + console.error('Failed to load wallet info:', err); + } + } + + async function loadUserSpaces(token) { + var spacesEl = document.getElementById('spaces-list'); + try { + var res = await fetch('/api/user/spaces', { + headers: { 'Authorization': 'Bearer ' + token }, + }); + var data = await res.json(); + if (!data.spaces || data.spaces.length === 0) { + spacesEl.innerHTML = '
You are not a member of any spaces yet.
'; + return; + } + + spacesEl.innerHTML = data.spaces.map(function(s) { + var spaceUrl = 'https://' + s.spaceSlug + '.rspace.online'; + var walletUrl = spaceUrl + '/rwallet'; + return '
' + + '' + + '
' + s.role + '
' + + '
'; + }).join(''); + } catch (err) { + spacesEl.innerHTML = '
Could not load spaces.
'; + console.error('Failed to load spaces:', err); + } } window.handleLogout = () => {