Merge branch 'dev' into main
Resolve login-button.ts conflicts — take dev's cleaner formatting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
037182efdf
|
|
@ -182,7 +182,10 @@ class FolkInboxClient extends HTMLElement {
|
|||
private async loadMailboxes() {
|
||||
try {
|
||||
const base = window.location.pathname.replace(/\/$/, "");
|
||||
const resp = await fetch(`${base}/api/mailboxes`);
|
||||
const token = getAccessToken();
|
||||
const resp = await fetch(`${base}/api/mailboxes`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
this.mailboxes = data.mailboxes || [];
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@
|
|||
|
||||
.cw-btn:hover {
|
||||
border-color: var(--rs-accent, #14b8a6);
|
||||
background: var(--rs-surface-hover, #2a2a40);
|
||||
background: var(--rs-bg-hover);
|
||||
}
|
||||
|
||||
.cw-btn:disabled {
|
||||
|
|
|
|||
|
|
@ -75,26 +75,26 @@ folk-campaign-workflow {
|
|||
|
||||
.cw-btn--back {
|
||||
text-decoration: none;
|
||||
color: #93c5fd;
|
||||
border-color: #3b82f644;
|
||||
color: var(--rs-primary-hover, #6366f1);
|
||||
border-color: var(--rs-border, rgba(59,130,246,0.27));
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.cw-btn--back:hover {
|
||||
background: #3b82f622;
|
||||
border-color: #3b82f6;
|
||||
background: var(--rs-bg-active, rgba(59,130,246,0.13));
|
||||
border-color: var(--rs-primary, #4f46e5);
|
||||
}
|
||||
|
||||
.cw-btn--run {
|
||||
background: #3b82f622;
|
||||
border-color: #3b82f655;
|
||||
color: #60a5fa;
|
||||
background: var(--rs-bg-active, rgba(59,130,246,0.13));
|
||||
border-color: var(--rs-border, rgba(59,130,246,0.33));
|
||||
color: var(--rs-primary-hover, #6366f1);
|
||||
}
|
||||
|
||||
.cw-btn--run:hover {
|
||||
background: #3b82f633;
|
||||
border-color: #3b82f6;
|
||||
background: var(--rs-bg-active, rgba(59,130,246,0.2));
|
||||
border-color: var(--rs-primary, #4f46e5);
|
||||
}
|
||||
|
||||
.cw-toggle {
|
||||
|
|
|
|||
|
|
@ -926,7 +926,13 @@ export function renderShell(opts: ShellOptions): string {
|
|||
const sp = document.querySelector('rstack-space-settings');
|
||||
if (sp) sp.setAttribute('module-id', moduleId);
|
||||
if (tabCache) {
|
||||
const switchId = moduleId; // capture for staleness check
|
||||
tabCache.switchTo(moduleId).then(ok => {
|
||||
// If user already clicked a different tab, don't navigate for this one
|
||||
if (currentModuleId !== switchId) {
|
||||
console.log('[shell] switchTo result:', ok, 'for', switchId, '(stale, user switched to', currentModuleId + ')');
|
||||
return;
|
||||
}
|
||||
console.log('[shell] switchTo result:', ok, 'for', moduleId);
|
||||
if (ok) {
|
||||
tabBar.setAttribute('active', layerId);
|
||||
|
|
@ -936,6 +942,7 @@ export function renderShell(opts: ShellOptions): string {
|
|||
window.location.href = url;
|
||||
}
|
||||
}).catch((err) => {
|
||||
if (currentModuleId !== switchId) return; // stale
|
||||
console.error('[shell] switchTo error:', err);
|
||||
window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
|
||||
});
|
||||
|
|
@ -1849,7 +1856,7 @@ const SUBNAV_CSS = `
|
|||
background: var(--rs-bg-surface);
|
||||
scrollbar-width: none;
|
||||
position: sticky;
|
||||
top: 92px;
|
||||
top: 93px;
|
||||
z-index: 100;
|
||||
}
|
||||
.rapp-subnav::-webkit-scrollbar { display: none; }
|
||||
|
|
@ -1949,7 +1956,7 @@ const TABBAR_CSS = `
|
|||
z-index: 100;
|
||||
}
|
||||
.rapp-tabbar::-webkit-scrollbar { display: none; }
|
||||
.rapp-subnav + .rapp-tabbar { top: 129px; }
|
||||
.rapp-subnav + .rapp-tabbar { top: 130px; }
|
||||
@media (max-width: 640px) {
|
||||
.rapp-tabbar {
|
||||
position: relative;
|
||||
|
|
|
|||
|
|
@ -394,18 +394,28 @@ export class RStackIdentity extends HTMLElement {
|
|||
async #validateSessionWithServer() {
|
||||
const session = getSession();
|
||||
if (!session?.accessToken) return;
|
||||
|
||||
// Throttle: skip if validated within the last 5 minutes
|
||||
const VALIDATE_KEY = "eid_last_validated";
|
||||
const VALIDATE_INTERVAL = 5 * 60 * 1000;
|
||||
const lastValidated = parseInt(localStorage.getItem(VALIDATE_KEY) || "0", 10);
|
||||
if (Date.now() - lastValidated < VALIDATE_INTERVAL) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${ENCRYPTID_URL}/api/session/verify`, {
|
||||
headers: { Authorization: `Bearer ${session.accessToken}` },
|
||||
});
|
||||
if (!res.ok) {
|
||||
// Session revoked — clear locally and re-render
|
||||
localStorage.removeItem(VALIDATE_KEY);
|
||||
localStorage.removeItem(SESSION_KEY);
|
||||
localStorage.removeItem("rspace-username");
|
||||
_removeSessionCookie();
|
||||
resetDocBridge();
|
||||
this.#render();
|
||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "revoked" } }));
|
||||
} else {
|
||||
localStorage.setItem(VALIDATE_KEY, String(Date.now()));
|
||||
}
|
||||
} catch { /* network error — let token expire naturally */ }
|
||||
}
|
||||
|
|
@ -470,7 +480,7 @@ export class RStackIdentity extends HTMLElement {
|
|||
.eid-nudge-link-row { display: flex; gap: 6px; margin-bottom: 0.4rem; }
|
||||
.eid-nudge-link-input {
|
||||
flex: 1; font-size: 0.72rem; padding: 5px 8px; border-radius: 6px;
|
||||
border: 1px solid var(--rs-border, #334155); background: var(--rs-bg-inset, #0f172a);
|
||||
border: 1px solid var(--rs-border, #334155); background: var(--rs-input-bg, #0f172a);
|
||||
color: var(--rs-text-secondary, #94a3b8); min-width: 0; outline: none;
|
||||
}
|
||||
.eid-nudge-btn--copy {
|
||||
|
|
@ -548,7 +558,7 @@ export class RStackIdentity extends HTMLElement {
|
|||
if (e.key === "encryptid_session" || e.key === PERSONAS_KEY) {
|
||||
this.#render();
|
||||
if (e.key === "encryptid_session") {
|
||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: e.newValue ? "signin" : "signout" } }));
|
||||
// Session cleared from another tab — reload to show logged-out state
|
||||
if (!e.newValue) window.location.reload();
|
||||
}
|
||||
|
|
@ -595,7 +605,7 @@ export class RStackIdentity extends HTMLElement {
|
|||
const payload = parseJWT(newToken);
|
||||
storeSession(newToken, (payload.username as string) || refreshData.username || username, (payload.did as string) || did);
|
||||
this.#render();
|
||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "refresh" } }));
|
||||
}
|
||||
} catch { /* offline — keep whatever we have */ }
|
||||
}
|
||||
|
|
@ -684,9 +694,7 @@ export class RStackIdentity extends HTMLElement {
|
|||
if (action === "signout") {
|
||||
clearSession();
|
||||
resetDocBridge();
|
||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
|
||||
// Reload so the server re-renders the current rApp in logged-out mode
|
||||
window.location.reload();
|
||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "signout" } }));
|
||||
return;
|
||||
} else if (action === "my-account") {
|
||||
this.showAccountModal();
|
||||
|
|
@ -715,7 +723,7 @@ export class RStackIdentity extends HTMLElement {
|
|||
clearSession();
|
||||
resetDocBridge();
|
||||
this.#render();
|
||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "persona-switch" } }));
|
||||
this.showAuthModal();
|
||||
} else if (action === "remove-persona") {
|
||||
const targetDid = (el as HTMLElement).dataset.did || "";
|
||||
|
|
@ -879,7 +887,7 @@ export class RStackIdentity extends HTMLElement {
|
|||
storeSession(data.token, data.username || "", data.did || "");
|
||||
close();
|
||||
this.#render();
|
||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "signin" } }));
|
||||
callbacks?.onSuccess?.();
|
||||
// Auto-redirect to personal space
|
||||
autoResolveSpace(data.token, data.username || "");
|
||||
|
|
@ -956,7 +964,7 @@ export class RStackIdentity extends HTMLElement {
|
|||
storeSession(data.token, username, data.did || "");
|
||||
close();
|
||||
this.#render();
|
||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
|
||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true, detail: { reason: "signin" } }));
|
||||
callbacks?.onSuccess?.();
|
||||
// Show post-signup prompt recommending second device before redirecting
|
||||
this.#showPostSignupPrompt(data.token, username);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export class TabCache {
|
|||
private currentModuleId: string;
|
||||
private spaceSlug: string;
|
||||
private panes = new Map<string, HTMLElement>();
|
||||
private fetchController: AbortController | null = null;
|
||||
|
||||
constructor(spaceSlug: string, moduleId: string) {
|
||||
this.spaceSlug = spaceSlug;
|
||||
|
|
@ -100,6 +101,12 @@ export class TabCache {
|
|||
|
||||
/** Switch to a module tab within the current space. Returns true if handled client-side. */
|
||||
async switchTo(moduleId: string): Promise<boolean> {
|
||||
// Abort any in-flight fetch from a previous switchTo call
|
||||
if (this.fetchController) {
|
||||
this.fetchController.abort();
|
||||
this.fetchController = null;
|
||||
}
|
||||
|
||||
const key = this.paneKey(this.spaceSlug, moduleId);
|
||||
if (moduleId === this.currentModuleId && this.panes.has(key)) {
|
||||
console.log("[TabCache] switchTo", moduleId, "→ already current + cached");
|
||||
|
|
@ -199,18 +206,25 @@ export class TabCache {
|
|||
this.hideAllPanes();
|
||||
app.appendChild(loadingPane);
|
||||
|
||||
// Create an abort controller that also times out after 15s
|
||||
const controller = new AbortController();
|
||||
this.fetchController = controller;
|
||||
const timeoutId = setTimeout(() => controller.abort(), 15_000);
|
||||
|
||||
try {
|
||||
const resp = await fetch(fetchUrl, {
|
||||
headers: { "Accept": "text/html" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!resp.ok) {
|
||||
clearTimeout(timeoutId);
|
||||
console.warn("[TabCache] fetchAndInject: HTTP", resp.status, "for", fetchUrl);
|
||||
loadingPane.remove();
|
||||
return false;
|
||||
}
|
||||
|
||||
const html = await resp.text();
|
||||
clearTimeout(timeoutId);
|
||||
console.log("[TabCache] fetchAndInject: got", html.length, "bytes for", moduleId);
|
||||
const content = this.extractContent(html);
|
||||
if (!content) {
|
||||
|
|
@ -247,8 +261,16 @@ export class TabCache {
|
|||
console.log("[TabCache] fetchAndInject: SUCCESS for", moduleId);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error("[TabCache] fetchAndInject: CATCH for", moduleId, err);
|
||||
clearTimeout(timeoutId);
|
||||
loadingPane.remove();
|
||||
// If aborted because the user switched to a different tab, return
|
||||
// "true" so the shell doesn't trigger a fallback full-page navigation
|
||||
// for a tab the user no longer wants.
|
||||
if (controller.signal.aborted && this.fetchController !== controller) {
|
||||
console.log("[TabCache] fetchAndInject: aborted (superseded) for", moduleId);
|
||||
return true;
|
||||
}
|
||||
console.error("[TabCache] fetchAndInject: CATCH for", moduleId, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -461,7 +461,7 @@ app.use('*', cors({
|
|||
}
|
||||
return undefined;
|
||||
},
|
||||
allowMethods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
|
||||
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowHeaders: ['Content-Type', 'Authorization'],
|
||||
credentials: true,
|
||||
}));
|
||||
|
|
@ -7141,21 +7141,8 @@ app.get('/', (c) => {
|
|||
<div id="error-msg" class="error"></div>
|
||||
<div id="success-msg" class="success"></div>
|
||||
|
||||
<!-- Sign-in mode (default) — passkey-first -->
|
||||
<div id="signin-fields">
|
||||
<button class="btn-primary" id="auth-btn" onclick="handleAuth()" style="font-size:1.1rem;padding:0.9rem">🔑 Sign in with Passkey</button>
|
||||
<div style="text-align:center;margin-top:0.75rem">
|
||||
<a href="#" id="show-email-fallback" onclick="toggleEmailFallback(); return false;" style="color:#64748b;font-size:0.8rem;text-decoration:none">No passkey on this device? Sign in with email</a>
|
||||
</div>
|
||||
<div id="email-fallback-section" style="display:none;margin-top:0.75rem;border-top:1px solid rgba(255,255,255,0.08);padding-top:0.75rem">
|
||||
<div class="form-group">
|
||||
<label for="signin-email">Email address</label>
|
||||
<input id="signin-email" type="email" placeholder="you@example.com" autocomplete="email" />
|
||||
</div>
|
||||
<button class="btn-secondary" id="magic-link-btn" onclick="sendMagicLink()">Send magic link</button>
|
||||
<div id="magic-link-msg" style="font-size:0.8rem;margin-top:0.5rem;display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sign-in mode (default) — dynamically rendered by renderSigninAccounts() -->
|
||||
<div id="signin-fields"></div>
|
||||
|
||||
<!-- Registration stepper (hidden until register tab) -->
|
||||
<div id="register-stepper" style="display:none">
|
||||
|
|
@ -7422,6 +7409,85 @@ app.get('/', (c) => {
|
|||
} from '/dist/index.js';
|
||||
|
||||
const TOKEN_KEY = 'encryptid_token';
|
||||
const KNOWN_ACCOUNTS_KEY = 'encryptid-known-accounts';
|
||||
|
||||
function getKnownAccounts() {
|
||||
try { return JSON.parse(localStorage.getItem(KNOWN_ACCOUNTS_KEY) || '[]'); }
|
||||
catch { return []; }
|
||||
}
|
||||
function addKnownAccount(username, displayName) {
|
||||
const accounts = getKnownAccounts().filter(a => a.username !== username);
|
||||
accounts.unshift({ username, displayName: displayName || username });
|
||||
localStorage.setItem(KNOWN_ACCOUNTS_KEY, JSON.stringify(accounts));
|
||||
}
|
||||
|
||||
// Render known accounts into sign-in fields on load
|
||||
function renderSigninAccounts() {
|
||||
const container = document.getElementById('signin-fields');
|
||||
const accounts = getKnownAccounts();
|
||||
if (accounts.length === 0) {
|
||||
// No known accounts — passkey-first button
|
||||
container.innerHTML = \`
|
||||
<button class="btn-primary" id="auth-btn" onclick="handleAuth()" style="font-size:1.1rem;padding:0.9rem">🔑 Sign in with Passkey</button>
|
||||
<div style="text-align:center;margin-top:0.75rem">
|
||||
<a href="#" id="show-email-fallback" onclick="toggleEmailFallback(); return false;" style="color:#64748b;font-size:0.8rem;text-decoration:none">No passkey on this device? Sign in with email</a>
|
||||
</div>
|
||||
<div id="email-fallback-section" style="display:none;margin-top:0.75rem;border-top:1px solid rgba(255,255,255,0.08);padding-top:0.75rem">
|
||||
<div class="form-group">
|
||||
<label for="signin-email">Email address</label>
|
||||
<input id="signin-email" type="email" placeholder="you@example.com" autocomplete="email" />
|
||||
</div>
|
||||
<button class="btn-secondary" id="magic-link-btn" onclick="sendMagicLink()">Send magic link</button>
|
||||
<div id="magic-link-msg" style="font-size:0.8rem;margin-top:0.5rem;display:none"></div>
|
||||
</div>\`;
|
||||
} else if (accounts.length === 1) {
|
||||
const esc = s => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
const name = esc(accounts[0].displayName || accounts[0].username);
|
||||
container.innerHTML = \`
|
||||
<button class="btn-primary" id="auth-btn" onclick="handleAuth('\${esc(accounts[0].username)}')" style="font-size:1.1rem;padding:0.9rem">🔑 Sign in as \${name}</button>
|
||||
<div style="text-align:center;margin-top:0.5rem">
|
||||
<a href="#" onclick="handleAuth(); return false;" style="color:#64748b;font-size:0.8rem;text-decoration:none">Use a different account</a>
|
||||
</div>
|
||||
<div style="text-align:center;margin-top:0.25rem">
|
||||
<a href="#" id="show-email-fallback" onclick="toggleEmailFallback(); return false;" style="color:#64748b;font-size:0.8rem;text-decoration:none">Sign in with email instead</a>
|
||||
</div>
|
||||
<div id="email-fallback-section" style="display:none;margin-top:0.75rem;border-top:1px solid rgba(255,255,255,0.08);padding-top:0.75rem">
|
||||
<div class="form-group">
|
||||
<label for="signin-email">Email address</label>
|
||||
<input id="signin-email" type="email" placeholder="you@example.com" autocomplete="email" />
|
||||
</div>
|
||||
<button class="btn-secondary" id="magic-link-btn" onclick="sendMagicLink()">Send magic link</button>
|
||||
<div id="magic-link-msg" style="font-size:0.8rem;margin-top:0.5rem;display:none"></div>
|
||||
</div>\`;
|
||||
} else {
|
||||
const esc = s => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
const items = accounts.map(a => {
|
||||
const name = esc(a.displayName || a.username);
|
||||
const initial = esc((a.displayName || a.username).slice(0, 2).toUpperCase());
|
||||
return \`<button class="btn-secondary" onclick="handleAuth('\${esc(a.username)}')" style="display:flex;align-items:center;gap:0.75rem;padding:0.75rem">
|
||||
<span style="width:28px;height:28px;border-radius:50%;background:#7c3aed;display:flex;align-items:center;justify-content:center;font-weight:600;font-size:0.75rem;color:#fff;flex-shrink:0">\${initial}</span>
|
||||
<span>\${name}</span>
|
||||
</button>\`;
|
||||
}).join('');
|
||||
container.innerHTML = \`
|
||||
<div style="display:flex;flex-direction:column;gap:0.5rem">\${items}</div>
|
||||
<div style="text-align:center;margin-top:0.5rem">
|
||||
<a href="#" onclick="handleAuth(); return false;" style="color:#64748b;font-size:0.8rem;text-decoration:none">Use a different account</a>
|
||||
</div>
|
||||
<div style="text-align:center;margin-top:0.25rem">
|
||||
<a href="#" id="show-email-fallback" onclick="toggleEmailFallback(); return false;" style="color:#64748b;font-size:0.8rem;text-decoration:none">Sign in with email instead</a>
|
||||
</div>
|
||||
<div id="email-fallback-section" style="display:none;margin-top:0.75rem;border-top:1px solid rgba(255,255,255,0.08);padding-top:0.75rem">
|
||||
<div class="form-group">
|
||||
<label for="signin-email">Email address</label>
|
||||
<input id="signin-email" type="email" placeholder="you@example.com" autocomplete="email" />
|
||||
</div>
|
||||
<button class="btn-secondary" id="magic-link-btn" onclick="sendMagicLink()">Send magic link</button>
|
||||
<div id="magic-link-msg" style="font-size:0.8rem;margin-top:0.5rem;display:none"></div>
|
||||
</div>\`;
|
||||
}
|
||||
}
|
||||
|
||||
let currentMode = 'signin';
|
||||
|
||||
// Registration stepper state
|
||||
|
|
@ -7692,19 +7758,19 @@ app.get('/', (c) => {
|
|||
}
|
||||
};
|
||||
|
||||
// handleAuth — always unscoped passkey picker (browser shows all stored passkeys)
|
||||
window.handleAuth = async () => {
|
||||
// handleAuth — optionally scoped to a known username, unscoped if omitted
|
||||
window.handleAuth = async (username) => {
|
||||
const btn = document.getElementById('auth-btn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Waiting for passkey...';
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Waiting for passkey...'; }
|
||||
hideMessages();
|
||||
|
||||
try {
|
||||
// Unscoped auth — let the browser show all available passkeys
|
||||
// If username provided, scope credentials to that user; otherwise unscoped
|
||||
const authBody = username ? { username } : {};
|
||||
const startRes = await fetch('/api/auth/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
body: JSON.stringify(authBody),
|
||||
});
|
||||
if (!startRes.ok) throw new Error('Failed to start authentication');
|
||||
const { options: serverOptions, prfSalt } = await startRes.json();
|
||||
|
|
@ -7782,18 +7848,22 @@ app.get('/', (c) => {
|
|||
}
|
||||
}
|
||||
|
||||
// Remember this account for next login
|
||||
if (data.username) addKnownAccount(data.username, data.username);
|
||||
|
||||
showProfile(data.token, data.username, data.did);
|
||||
} catch (err) {
|
||||
if (err.name === 'NotAllowedError') {
|
||||
showError('No passkey found on this device. Use email to sign in.');
|
||||
// Auto-show email fallback
|
||||
document.getElementById('email-fallback-section').style.display = 'block';
|
||||
document.getElementById('show-email-fallback').style.display = 'none';
|
||||
const fb = document.getElementById('email-fallback-section');
|
||||
const link = document.getElementById('show-email-fallback');
|
||||
if (fb) fb.style.display = 'block';
|
||||
if (link) link.style.display = 'none';
|
||||
} else {
|
||||
showError(err.message || 'Authentication failed');
|
||||
}
|
||||
btn.innerHTML = '🔑 Sign in with Passkey';
|
||||
btn.disabled = false;
|
||||
if (btn) { btn.innerHTML = '🔑 Sign in with Passkey'; btn.disabled = false; }
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -8334,8 +8404,10 @@ app.get('/', (c) => {
|
|||
}
|
||||
}
|
||||
|
||||
// On page load: handle query params and check for existing token
|
||||
// On page load: render known accounts + handle query params + check token
|
||||
(async () => {
|
||||
renderSigninAccounts();
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
if (params.get('tab') === 'register') switchTab('register');
|
||||
|
||||
|
|
|
|||
|
|
@ -361,25 +361,6 @@ const styles = `
|
|||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.email-fallback-link {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--eid-text-secondary);
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.email-fallback-link:hover {
|
||||
color: var(--eid-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.email-fallback-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -536,7 +517,7 @@ export class EncryptIDLoginButton extends HTMLElement {
|
|||
${PASSKEY_ICON}
|
||||
<span>${this.label}</span>
|
||||
</button>
|
||||
${this.showEmailFallback ? '' : `<button class="email-fallback-link" data-action="show-email">No passkey? Sign in with email</button>`}
|
||||
${this.showEmailFallback ? '' : `<button class="alt-action" data-action="show-email">No passkey? Sign in with email</button>`}
|
||||
${emailSection}
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -641,22 +622,18 @@ export class EncryptIDLoginButton extends HTMLElement {
|
|||
this.render();
|
||||
});
|
||||
|
||||
// Send magic link button
|
||||
// Send magic link button + enter key
|
||||
this.shadow.querySelector('[data-action="send-magic-link"]')?.addEventListener('click', () => {
|
||||
this.sendMagicLink();
|
||||
});
|
||||
|
||||
// Email input enter key
|
||||
const emailInput = this.shadow.querySelector('[data-email-input]') as HTMLInputElement;
|
||||
if (emailInput) {
|
||||
emailInput.addEventListener('keydown', (e) => {
|
||||
if ((e as KeyboardEvent).key === 'Enter') this.sendMagicLink();
|
||||
});
|
||||
}
|
||||
emailInput?.addEventListener('keydown', (e) => {
|
||||
if ((e as KeyboardEvent).key === 'Enter') this.sendMagicLink();
|
||||
});
|
||||
|
||||
// Login button with scoped username (1 known account)
|
||||
const loginBtn = this.shadow.querySelector('.login-btn:not([data-action])');
|
||||
if (loginBtn && !usernameInput) {
|
||||
if (loginBtn) {
|
||||
loginBtn.addEventListener('click', () => {
|
||||
const username = (loginBtn as HTMLElement).dataset.username;
|
||||
this.handleLogin(username);
|
||||
|
|
|
|||
|
|
@ -117,13 +117,18 @@ if (spaceSlug) {
|
|||
}
|
||||
|
||||
// Reload space list when user signs in/out (to show/hide private spaces)
|
||||
document.addEventListener("auth-change", () => {
|
||||
document.addEventListener("auth-change", (e) => {
|
||||
const reason = (e as CustomEvent).detail?.reason;
|
||||
|
||||
// Token refreshes are invisible — no UI side-effects needed
|
||||
if (reason === "refresh") return;
|
||||
|
||||
// Reload space switcher on state-changing events
|
||||
const spaceSwitcher = document.querySelector("rstack-space-switcher") as any;
|
||||
spaceSwitcher?.reload?.();
|
||||
|
||||
// If signed out, redirect to homepage
|
||||
const session = localStorage.getItem("encryptid_session");
|
||||
if (!session) {
|
||||
// Only redirect to homepage on genuine sign-out or server revocation
|
||||
if (reason === "signout" || reason === "revoked") {
|
||||
window.location.href = "/";
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue