feat(identity): store verified email on profile + show in account modal

Email verification wrote to the `email` column but account status read
from `profile_email` — now setUserEmail writes both. Account modal email
section displays the verified address when collapsed. Tour finale step
triggers identity setup on completion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-22 15:49:09 -07:00
parent 4c1cd21b8c
commit 29c4f48634
2 changed files with 7 additions and 4 deletions

View File

@ -711,7 +711,7 @@ export class RStackIdentity extends HTMLElement {
let openSection: string | null = null; let openSection: string | null = null;
// Account completion status // Account completion status
let acctStatus: { email: boolean; multiDevice: boolean; socialRecovery: boolean; guardianCount: number; credentialCount: number } | null = null; let acctStatus: { email: boolean; emailAddress?: string | null; multiDevice: boolean; socialRecovery: boolean; guardianCount: number; credentialCount: number } | null = null;
// Lazy-loaded data // Lazy-loaded data
let guardians: { id: string; name: string; email?: string; status: string }[] = []; let guardians: { id: string; name: string; email?: string; status: string }[] = [];
@ -819,10 +819,13 @@ export class RStackIdentity extends HTMLElement {
</div>`; </div>`;
} }
} }
const emailDisplay = !isOpen && done && acctStatus?.emailAddress
? `<span style="color:var(--rs-text-secondary);font-size:0.85rem;margin-left:8px">${acctStatus.emailAddress}</span>`
: "";
return ` return `
<div class="account-section${isOpen ? " open" : ""}${done === false ? " section--warning" : ""}"> <div class="account-section${isOpen ? " open" : ""}${done === false ? " section--warning" : ""}">
<div class="account-section-header" data-section="email"> <div class="account-section-header" data-section="email">
<span>${statusDot(done)} Email</span> <span>${statusDot(done)} Email${emailDisplay}</span>
<span class="section-arrow">${isOpen ? "▾" : "▸"}</span> <span class="section-arrow">${isOpen ? "▾" : "▸"}</span>
</div> </div>
${body} ${body}
@ -1087,7 +1090,7 @@ export class RStackIdentity extends HTMLElement {
body: JSON.stringify({ email: emailAddr, code }), body: JSON.stringify({ email: emailAddr, code }),
}); });
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || "Verification failed"); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || "Verification failed");
if (acctStatus) acctStatus.email = true; if (acctStatus) { acctStatus.email = true; acctStatus.emailAddress = emailAddr; }
openSection = null; render(); openSection = null; render();
this.dispatchEvent(new CustomEvent("identity-action", { bubbles: true, composed: true, detail: { action: "email-added", email: emailAddr } })); this.dispatchEvent(new CustomEvent("identity-action", { bubbles: true, composed: true, detail: { action: "email-added", email: emailAddr } }));
} catch (e: any) { } catch (e: any) {

View File

@ -224,7 +224,7 @@ export async function cleanExpiredChallenges(): Promise<number> {
// ============================================================================ // ============================================================================
export async function setUserEmail(userId: string, email: string): Promise<void> { export async function setUserEmail(userId: string, email: string): Promise<void> {
await sql`UPDATE users SET email = ${email} WHERE id = ${userId}`; await sql`UPDATE users SET email = ${email}, profile_email = ${email}, updated_at = NOW() WHERE id = ${userId}`;
} }
export async function getUserByEmail(email: string) { export async function getUserByEmail(email: string) {