feat(rinbox): add email forwarding prompt banner and fix auth token reading
Replace broken encryptid-token localStorage reads with getAccessToken/getUsername from rspace-header. Add forwarding status check against EncryptID API with enable/disable/dismiss banner on mailboxes view. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
92df7d332d
commit
1c93e3bb67
|
|
@ -10,6 +10,7 @@ import { mailboxSchema, type MailboxDoc } from "../schemas";
|
|||
import type { DocumentId } from "../../../shared/local-first/document";
|
||||
import { TourEngine } from "../../../shared/tour-engine";
|
||||
import { ViewHistory } from "../../../shared/view-history.js";
|
||||
import { getAccessToken, getUsername } from "../../../lib/rspace-header";
|
||||
|
||||
type ComposeMode = 'new' | 'reply' | 'reply-all' | 'forward';
|
||||
|
||||
|
|
@ -35,6 +36,12 @@ class FolkInboxClient extends HTMLElement {
|
|||
private _usernameCache = new Map<string, string>();
|
||||
private _currentUsername: string | null = null;
|
||||
private _history = new ViewHistory<"mailboxes" | "threads" | "thread" | "approvals" | "personal" | "agents">("mailboxes");
|
||||
private _fwdStatus: 'loading' | 'unavailable' | 'no-email' | 'ready' | 'enabled' | 'error' = 'loading';
|
||||
private _fwdAddress = '';
|
||||
private _fwdTarget = '';
|
||||
private _fwdDismissed = false;
|
||||
private _fwdBusy = false;
|
||||
private _fwdError = '';
|
||||
private demoApprovals: any[] = [
|
||||
{
|
||||
id: "a1", subject: "Re: Q1 Budget approval for rSpace infrastructure", status: "PENDING",
|
||||
|
|
@ -106,6 +113,7 @@ class FolkInboxClient extends HTMLElement {
|
|||
connectedCallback() {
|
||||
this.space = this.getAttribute("space") || "demo";
|
||||
this._loadUsername();
|
||||
this._checkForwardStatus();
|
||||
if (this.space === "demo") { this.loadDemoData(); }
|
||||
else { this.subscribeOffline(); this.loadMailboxes(); }
|
||||
// Auto-start tour on first visit
|
||||
|
|
@ -119,14 +127,9 @@ class FolkInboxClient extends HTMLElement {
|
|||
this._offlineUnsubs = [];
|
||||
}
|
||||
|
||||
/** Extract username from EncryptID JWT in localStorage */
|
||||
/** Extract username from EncryptID session */
|
||||
private _loadUsername() {
|
||||
try {
|
||||
const token = localStorage.getItem('encryptid-token');
|
||||
if (!token) return;
|
||||
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||
if (payload.username) this._currentUsername = payload.username;
|
||||
} catch { /* no token or invalid */ }
|
||||
this._currentUsername = getUsername();
|
||||
}
|
||||
|
||||
/** Resolve a DID to a display name */
|
||||
|
|
@ -253,7 +256,7 @@ class FolkInboxClient extends HTMLElement {
|
|||
}
|
||||
try {
|
||||
const base = window.location.pathname.replace(/\/$/, "");
|
||||
const token = localStorage.getItem('encryptid-token');
|
||||
const token = getAccessToken();
|
||||
const resp = await fetch(`${base}/api/personal-inboxes`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
|
|
@ -286,11 +289,162 @@ class FolkInboxClient extends HTMLElement {
|
|||
|
||||
private getAuthHeaders(): Record<string, string> {
|
||||
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
||||
const token = localStorage.getItem('encryptid-token');
|
||||
const token = getAccessToken();
|
||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||
return headers;
|
||||
}
|
||||
|
||||
private async _checkForwardStatus() {
|
||||
if (sessionStorage.getItem('rinbox_fwd_dismissed')) {
|
||||
this._fwdDismissed = true;
|
||||
this._fwdStatus = 'unavailable';
|
||||
return;
|
||||
}
|
||||
const token = getAccessToken();
|
||||
if (!token) { this._fwdStatus = 'unavailable'; return; }
|
||||
try {
|
||||
const resp = await fetch('https://auth.rspace.online/api/account/email-forward', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!resp.ok) { this._fwdStatus = 'unavailable'; return; }
|
||||
const data = await resp.json();
|
||||
if (data.enabled) {
|
||||
this._fwdStatus = 'enabled';
|
||||
this._fwdAddress = data.address || '';
|
||||
this._fwdTarget = data.target || '';
|
||||
} else if (data.available && data.target) {
|
||||
this._fwdStatus = 'ready';
|
||||
this._fwdAddress = data.address || '';
|
||||
this._fwdTarget = data.target || '';
|
||||
} else if (!data.target) {
|
||||
this._fwdStatus = 'no-email';
|
||||
this._fwdAddress = data.address || '';
|
||||
} else {
|
||||
this._fwdStatus = 'unavailable';
|
||||
}
|
||||
} catch {
|
||||
this._fwdStatus = 'unavailable';
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
private async _enableForwarding() {
|
||||
if (this._fwdBusy) return;
|
||||
this._fwdBusy = true;
|
||||
this._fwdError = '';
|
||||
this.render();
|
||||
const token = getAccessToken();
|
||||
try {
|
||||
const resp = await fetch('https://auth.rspace.online/api/account/email-forward/enable', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
});
|
||||
if (resp.ok) {
|
||||
this._fwdStatus = 'enabled';
|
||||
} else {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
this._fwdError = (err as any).error || 'Failed to enable forwarding';
|
||||
this._fwdStatus = 'error';
|
||||
}
|
||||
} catch {
|
||||
this._fwdError = 'Network error — try again';
|
||||
this._fwdStatus = 'error';
|
||||
}
|
||||
this._fwdBusy = false;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private async _disableForwarding() {
|
||||
if (this._fwdBusy) return;
|
||||
this._fwdBusy = true;
|
||||
this.render();
|
||||
const token = getAccessToken();
|
||||
try {
|
||||
const resp = await fetch('https://auth.rspace.online/api/account/email-forward/disable', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
});
|
||||
if (resp.ok) {
|
||||
this._fwdStatus = 'ready';
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
this._fwdBusy = false;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private _dismissForwardBanner() {
|
||||
this._fwdDismissed = true;
|
||||
sessionStorage.setItem('rinbox_fwd_dismissed', '1');
|
||||
this.render();
|
||||
}
|
||||
|
||||
private renderForwardBanner(): string {
|
||||
if (this._fwdDismissed || this._fwdStatus === 'loading' || this._fwdStatus === 'unavailable') return '';
|
||||
|
||||
if (this._fwdStatus === 'ready') {
|
||||
return `
|
||||
<div class="fwd-banner fwd-ready">
|
||||
<div class="fwd-banner-content">
|
||||
<div class="fwd-banner-title">Forward rInbox to your email</div>
|
||||
<div class="fwd-banner-route">
|
||||
<code>${this.escapeHtml(this._fwdAddress)}</code>
|
||||
<span class="fwd-arrow">→</span>
|
||||
<code>${this.escapeHtml(this._fwdTarget)}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fwd-banner-actions">
|
||||
<button class="fwd-btn-enable" data-action="fwd-enable" ${this._fwdBusy ? 'disabled' : ''}>${this._fwdBusy ? 'Enabling...' : 'Enable Forwarding'}</button>
|
||||
<button class="fwd-btn-dismiss" data-action="fwd-dismiss">Dismiss</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (this._fwdStatus === 'enabled') {
|
||||
return `
|
||||
<div class="fwd-banner fwd-enabled">
|
||||
<div class="fwd-banner-content">
|
||||
<div class="fwd-banner-title">Forwarding active</div>
|
||||
<div class="fwd-banner-route">
|
||||
<code>${this.escapeHtml(this._fwdAddress)}</code>
|
||||
<span class="fwd-arrow">→</span>
|
||||
<code>${this.escapeHtml(this._fwdTarget)}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fwd-banner-actions">
|
||||
<button class="fwd-btn-disable" data-action="fwd-disable" ${this._fwdBusy ? 'disabled' : ''}>${this._fwdBusy ? 'Disabling...' : 'Disable'}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (this._fwdStatus === 'no-email') {
|
||||
return `
|
||||
<div class="fwd-banner fwd-no-email">
|
||||
<div class="fwd-banner-content">
|
||||
<div class="fwd-banner-title">Forward rInbox to your email</div>
|
||||
<div class="fwd-banner-desc">Add a verified email to your profile to enable forwarding.</div>
|
||||
</div>
|
||||
<div class="fwd-banner-actions">
|
||||
<a class="fwd-btn-profile" href="https://auth.rspace.online/profile" target="_blank" rel="noopener">Go to Profile</a>
|
||||
<button class="fwd-btn-dismiss" data-action="fwd-dismiss">Dismiss</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (this._fwdStatus === 'error') {
|
||||
return `
|
||||
<div class="fwd-banner fwd-error">
|
||||
<div class="fwd-banner-content">
|
||||
<div class="fwd-banner-title">${this.escapeHtml(this._fwdError)}</div>
|
||||
</div>
|
||||
<div class="fwd-banner-actions">
|
||||
<button class="fwd-btn-dismiss" data-action="fwd-dismiss">Dismiss</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private timeAgo(dateStr: string): string {
|
||||
const diff = Date.now() - new Date(dateStr).getTime();
|
||||
const mins = Math.floor(diff / 60000);
|
||||
|
|
@ -509,12 +663,40 @@ class FolkInboxClient extends HTMLElement {
|
|||
|
||||
.sample-banner { padding: 8px 16px; background: rgba(99,102,241,0.12); border: 1px solid rgba(99,102,241,0.25); border-radius: 8px; color: #a5b4fc; font-size: 13px; text-align: center; margin-bottom: 12px; }
|
||||
|
||||
/* Forwarding banner */
|
||||
.fwd-banner { display: flex; align-items: center; gap: 1rem; padding: 0.75rem 1rem; border-radius: 10px; margin-bottom: 12px; border: 1px solid; }
|
||||
.fwd-banner.fwd-ready { background: rgba(99,102,241,0.08); border-color: rgba(99,102,241,0.25); }
|
||||
.fwd-banner.fwd-enabled { background: rgba(34,197,94,0.08); border-color: rgba(34,197,94,0.25); }
|
||||
.fwd-banner.fwd-no-email { background: rgba(251,191,36,0.08); border-color: rgba(251,191,36,0.25); }
|
||||
.fwd-banner.fwd-error { background: rgba(239,68,68,0.08); border-color: rgba(239,68,68,0.25); }
|
||||
.fwd-banner-content { flex: 1; min-width: 0; }
|
||||
.fwd-banner-title { font-size: 0.85rem; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 2px; }
|
||||
.fwd-banner-desc { font-size: 0.8rem; color: var(--rs-text-secondary); }
|
||||
.fwd-banner-route { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
|
||||
.fwd-banner-route code { font-size: 0.75rem; color: #818cf8; background: rgba(99,102,241,0.1); padding: 2px 6px; border-radius: 4px; }
|
||||
.fwd-enabled .fwd-banner-route code { color: #4ade80; background: rgba(34,197,94,0.1); }
|
||||
.fwd-arrow { color: var(--rs-text-muted); font-size: 0.8rem; }
|
||||
.fwd-banner-actions { display: flex; gap: 0.5rem; align-items: center; flex-shrink: 0; }
|
||||
.fwd-btn-enable { padding: 6px 14px; border-radius: 6px; border: none; background: linear-gradient(135deg, #6366f1, #0891b2); color: white; cursor: pointer; font-size: 0.8rem; font-weight: 600; transition: opacity 0.15s; }
|
||||
.fwd-btn-enable:hover { opacity: 0.9; }
|
||||
.fwd-btn-enable:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.fwd-btn-disable { padding: 6px 12px; border-radius: 6px; border: 1px solid rgba(34,197,94,0.3); background: transparent; color: #4ade80; cursor: pointer; font-size: 0.75rem; transition: all 0.15s; }
|
||||
.fwd-btn-disable:hover { border-color: rgba(34,197,94,0.6); background: rgba(34,197,94,0.1); }
|
||||
.fwd-btn-disable:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.fwd-btn-dismiss { padding: 6px 10px; border-radius: 6px; border: none; background: transparent; color: var(--rs-text-muted); cursor: pointer; font-size: 0.75rem; transition: color 0.15s; }
|
||||
.fwd-btn-dismiss:hover { color: var(--rs-text-secondary); }
|
||||
.fwd-btn-profile { padding: 6px 14px; border-radius: 6px; border: 1px solid rgba(251,191,36,0.3); background: rgba(251,191,36,0.1); color: #fbbf24; text-decoration: none; font-size: 0.8rem; font-weight: 500; transition: all 0.15s; }
|
||||
.fwd-btn-profile:hover { border-color: rgba(251,191,36,0.5); background: rgba(251,191,36,0.15); }
|
||||
.fwd-error .fwd-banner-title { color: #f87171; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mailbox-grid { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
|
||||
.help-grid { grid-template-columns: 1fr; }
|
||||
.thread-row { flex-wrap: wrap; gap: 4px; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.fwd-banner { flex-direction: column; align-items: stretch; gap: 0.5rem; }
|
||||
.fwd-banner-actions { justify-content: flex-end; }
|
||||
.rapp-nav { gap: 0.25rem; }
|
||||
.nav-btn { padding: 4px 8px; font-size: 12px; }
|
||||
.inbox-header { padding: 0.625rem 0.75rem; gap: 0.5rem; }
|
||||
|
|
@ -533,6 +715,7 @@ class FolkInboxClient extends HTMLElement {
|
|||
<div class="container">
|
||||
${this.renderNav()}
|
||||
${this.showingSampleData ? '<div class="sample-banner">Showing sample data — create a mailbox to get started</div>' : ''}
|
||||
${this.view === 'mailboxes' ? this.renderForwardBanner() : ''}
|
||||
${this.renderView()}
|
||||
${this.helpOpen ? this.renderHelp() : ""}
|
||||
</div>
|
||||
|
|
@ -1154,6 +1337,11 @@ class FolkInboxClient extends HTMLElement {
|
|||
helpClose.addEventListener("click", () => { this.helpOpen = false; this.render(); });
|
||||
}
|
||||
|
||||
// Forwarding banner actions
|
||||
this.shadow.querySelector("[data-action='fwd-enable']")?.addEventListener("click", () => this._enableForwarding());
|
||||
this.shadow.querySelector("[data-action='fwd-disable']")?.addEventListener("click", () => this._disableForwarding());
|
||||
this.shadow.querySelector("[data-action='fwd-dismiss']")?.addEventListener("click", () => this._dismissForwardBanner());
|
||||
|
||||
// Mailbox click
|
||||
this.shadow.querySelectorAll("[data-mailbox]").forEach((card) => {
|
||||
card.addEventListener("click", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue