/** * — Payments dashboard showing requests in/out. * * Tabs: * - Payments In: payment requests the user created (money coming to them) * - Payments Out: placeholder for future on-chain tx tracking */ interface PaymentRow { id: string; description: string; amount: string; amountEditable: boolean; token: string; status: string; paymentType: string; paymentCount: number; maxPayments: number; created_at: string; paid_at: string | null; } class FolkPaymentsDashboard extends HTMLElement { private shadow: ShadowRoot; private space = 'default'; private authenticated = false; private loading = true; private error = ''; private activeTab: 'in' | 'out' = 'in'; private payments: PaymentRow[] = []; constructor() { super(); this.shadow = this.attachShadow({ mode: 'open' }); } connectedCallback() { this.space = this.getAttribute('space') || 'default'; this.render(); this.checkSessionAndLoad(); } private getApiBase(): string { const path = window.location.pathname; const match = path.match(/^(\/[^/]+)?\/rcart/); return match ? match[0] : '/rcart'; } private getSpacePrefix(): string { const path = window.location.pathname; const match = path.match(/^\/([^/]+)\/rcart/); return match ? `/${match[1]}` : ''; } private async checkSessionAndLoad() { try { const { getSessionManager } = await import('../../../src/encryptid/session'); const { getSession } = await import('../../../shared/components/rstack-identity'); const sm = getSessionManager(); const rstackSession = getSession(); if (sm.isValid() || !!rstackSession) { this.authenticated = true; await this.fetchPayments(); } else { this.loading = false; this.render(); } } catch { this.loading = false; this.render(); } } private async fetchPayments() { this.loading = true; this.error = ''; this.render(); try { const { getSessionManager } = await import('../../../src/encryptid/session'); const { getSession: getRstackSession } = await import('../../../shared/components/rstack-identity'); const session = getSessionManager(); const accessToken = session.getSession()?.accessToken || getRstackSession()?.accessToken; const res = await fetch(`${this.getApiBase()}/api/payments`, { headers: accessToken ? { 'Authorization': `Bearer ${accessToken}` } : {}, }); if (!res.ok) { if (res.status === 401) { this.authenticated = false; this.loading = false; this.render(); return; } throw new Error('Failed to load payments'); } const data = await res.json(); this.payments = data.payments || []; } catch (e) { this.error = e instanceof Error ? e.message : String(e); } this.loading = false; this.render(); } private render() { this.shadow.innerHTML = `
${this.activeTab === 'in' ? this.renderPaymentsIn() : this.renderPaymentsOut()}
`; this.bindEvents(); } private renderPaymentsIn(): string { if (!this.authenticated) { return `
🔒

Sign in to view your payments

Authenticate with your passkey to see payment requests you've created.

`; } if (this.loading) { return '
Loading payments...
'; } if (this.error) { return `
${this.esc(this.error)}
`; } if (this.payments.length === 0) { return `
💳

No payment requests yet

Create a payment request to generate a QR code anyone can scan to pay you.

Create your first request
`; } return `
${this.payments.map(p => this.renderPaymentCard(p)).join('')}
`; } private renderPaymentCard(p: PaymentRow): string { const amountDisplay = p.amountEditable && (!p.amount || p.amount === '0') ? 'Any amount' : `${p.amount} ${p.token}`; const date = new Date(p.created_at); const dateStr = date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); const statusClass = this.getStatusClass(p.status); const statusLabel = p.status.charAt(0).toUpperCase() + p.status.slice(1); const typeLabel = p.paymentType === 'subscription' ? 'Recurring' : p.paymentType === 'payer_choice' ? 'Flexible' : 'One-time'; return `
${this.esc(p.description)}
${dateStr} ${typeLabel} ${p.paymentCount > 0 ? `${p.paymentCount} paid` : ''}
${amountDisplay}
${statusLabel}
`; } private renderPaymentsOut(): string { return `
🚀

Coming soon

Outbound payment tracking will show on-chain transactions from your wallet.

`; } private getStatusClass(status: string): string { switch (status) { case 'pending': return 'status-pending'; case 'paid': return 'status-paid'; case 'expired': return 'status-expired'; case 'cancelled': return 'status-cancelled'; case 'filled': return 'status-filled'; default: return 'status-pending'; } } private bindEvents() { this.shadow.querySelectorAll('[data-tab]').forEach(el => { el.addEventListener('click', () => { this.activeTab = (el as HTMLElement).dataset.tab as 'in' | 'out'; this.render(); }); }); this.shadow.querySelector('[data-action="retry"]')?.addEventListener('click', () => this.fetchPayments()); } private getStyles(): string { return ` :host { display: block; padding: 1.5rem; width: 100%; max-width: 720px; } * { box-sizing: border-box; } .header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem; gap: 1rem; flex-wrap: wrap; } .title { color: var(--rs-text-primary); font-size: 1.5rem; font-weight: 700; margin: 0; } .btn { padding: 0.5rem 1rem; border-radius: 8px; border: 1px solid var(--rs-border); background: var(--rs-bg-surface); color: var(--rs-text-primary); cursor: pointer; font-size: 0.875rem; font-weight: 500; text-decoration: none; text-align: center; display: inline-block; white-space: nowrap; } .btn:hover { border-color: var(--rs-border-strong); } .btn-primary { background: var(--rs-primary-hover); border-color: var(--rs-primary); color: #fff; } .btn-primary:hover { background: #4338ca; } .btn-sm { padding: 0.25rem 0.625rem; font-size: 0.75rem; } .tabs { display: flex; border-bottom: 1px solid var(--rs-border); margin-bottom: 1rem; gap: 0; } .tab { padding: 0.625rem 1rem; border: none; background: none; color: var(--rs-text-secondary); cursor: pointer; font-size: 0.875rem; font-weight: 500; border-bottom: 2px solid transparent; transition: all 0.15s; display: flex; align-items: center; gap: 0.375rem; } .tab:hover { color: var(--rs-text-primary); } .tab.active { color: var(--rs-text-primary); border-bottom-color: var(--rs-primary-hover); } .tab-count { background: var(--rs-bg-hover); color: var(--rs-text-secondary); padding: 0.0625rem 0.375rem; border-radius: 999px; font-size: 0.6875rem; font-weight: 600; } .tab.active .tab-count { background: rgba(99,102,241,0.15); color: var(--rs-primary-hover); } .loading { text-align: center; color: var(--rs-text-secondary); padding: 3rem 1rem; font-size: 0.9375rem; } .error-msg { color: #f87171; text-align: center; padding: 2rem 1rem; font-size: 0.875rem; display: flex; flex-direction: column; align-items: center; gap: 0.75rem; } .retry-btn { margin-top: 0.25rem; } .empty-state { text-align: center; padding: 3rem 1.5rem; } .empty-icon { font-size: 2.5rem; margin-bottom: 0.75rem; } .empty-title { color: var(--rs-text-primary); font-size: 1.125rem; font-weight: 600; margin: 0 0 0.375rem; } .empty-desc { color: var(--rs-text-secondary); font-size: 0.875rem; margin: 0 0 1.25rem; line-height: 1.5; } .payment-list { display: flex; flex-direction: column; gap: 0.5rem; } .payment-card { display: flex; align-items: center; justify-content: space-between; padding: 0.875rem 1rem; background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 10px; cursor: pointer; transition: border-color 0.15s, background 0.15s; text-decoration: none; color: inherit; gap: 1rem; } .payment-card:hover { border-color: var(--rs-border-strong); background: var(--rs-bg-hover); } .card-main { flex: 1; min-width: 0; } .card-desc { color: var(--rs-text-primary); font-size: 0.9375rem; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .card-meta { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.25rem; flex-wrap: wrap; } .card-date { color: var(--rs-text-muted); font-size: 0.75rem; } .card-type { color: var(--rs-text-secondary); font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; background: var(--rs-bg-hover); padding: 0.0625rem 0.375rem; border-radius: 4px; } .card-count { color: var(--rs-text-secondary); font-size: 0.75rem; } .card-right { display: flex; flex-direction: column; align-items: flex-end; gap: 0.375rem; flex-shrink: 0; } .card-amount { color: var(--rs-text-primary); font-size: 1rem; font-weight: 600; white-space: nowrap; } .status-badge { padding: 0.125rem 0.5rem; border-radius: 999px; font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; } .status-pending { background: rgba(250,204,21,0.15); color: #ca8a04; } .status-paid { background: rgba(34,197,94,0.15); color: #16a34a; } .status-expired { background: rgba(156,163,175,0.15); color: #6b7280; } .status-cancelled { background: rgba(239,68,68,0.15); color: #dc2626; } .status-filled { background: rgba(59,130,246,0.15); color: #2563eb; } @media (max-width: 480px) { :host { padding: 1rem; } .header { flex-direction: column; align-items: stretch; } .payment-card { flex-direction: column; align-items: stretch; gap: 0.5rem; } .card-right { flex-direction: row; align-items: center; justify-content: space-between; } } `; } private esc(s: string): string { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } } customElements.define('folk-payments-dashboard', FolkPaymentsDashboard);