feat(encryptid): periodic nudge toast for users without a second device
Shows a dismissible toast notification 3s after page load when the user has only one passkey. Links directly to the device section in My Account. Dismissal remembered for 7 days via localStorage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cd6d979b5a
commit
50edf06900
|
|
@ -369,6 +369,9 @@ export class RStackIdentity extends HTMLElement {
|
|||
// Validate session with server — detects logout from another browser session
|
||||
this.#validateSessionWithServer();
|
||||
|
||||
// Nudge users without a second device to link one
|
||||
this.#checkDeviceNudge();
|
||||
|
||||
// Propagate login/logout across tabs via storage events
|
||||
window.addEventListener("storage", this.#onStorageChange);
|
||||
}
|
||||
|
|
@ -396,6 +399,86 @@ export class RStackIdentity extends HTMLElement {
|
|||
} catch { /* network error — let token expire naturally */ }
|
||||
}
|
||||
|
||||
async #checkDeviceNudge() {
|
||||
const session = getSession();
|
||||
if (!session?.accessToken) return;
|
||||
|
||||
// Don't nag if dismissed within the last 7 days
|
||||
const NUDGE_KEY = "eid_device_nudge_dismissed";
|
||||
const dismissed = localStorage.getItem(NUDGE_KEY);
|
||||
if (dismissed && Date.now() - parseInt(dismissed, 10) < 7 * 24 * 60 * 60 * 1000) return;
|
||||
|
||||
// Wait a moment so it doesn't compete with page load
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
|
||||
// Fetch account status
|
||||
try {
|
||||
const res = await fetch(`${ENCRYPTID_URL}/api/account/status`, {
|
||||
headers: { Authorization: `Bearer ${getAccessToken()}` },
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const status = await res.json();
|
||||
if (status.multiDevice) return; // already has 2+ devices
|
||||
|
||||
// Show a toast nudge
|
||||
const toast = document.createElement("div");
|
||||
toast.className = "eid-device-nudge";
|
||||
toast.innerHTML = `
|
||||
<style>
|
||||
.eid-device-nudge {
|
||||
position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 9999;
|
||||
background: var(--rs-bg-surface, #1e1e2e); border: 1px solid rgba(6,182,212,0.4);
|
||||
border-radius: 14px; padding: 1rem 1.25rem; max-width: 340px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.4); animation: eid-nudge-in 0.4s ease-out;
|
||||
color: var(--rs-text-primary, #e2e8f0); font-family: system-ui, sans-serif;
|
||||
}
|
||||
.eid-nudge-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
||||
.eid-nudge-icon { font-size: 1.5rem; }
|
||||
.eid-nudge-title { font-weight: 600; font-size: 0.95rem; }
|
||||
.eid-nudge-body { font-size: 0.85rem; color: var(--rs-text-secondary, #94a3b8); line-height: 1.5; margin-bottom: 0.75rem; }
|
||||
.eid-nudge-actions { display: flex; gap: 8px; }
|
||||
.eid-nudge-btn {
|
||||
padding: 8px 14px; border-radius: 8px; border: none; font-size: 0.85rem;
|
||||
font-weight: 600; cursor: pointer; transition: all 0.2s;
|
||||
}
|
||||
.eid-nudge-btn--primary {
|
||||
background: linear-gradient(135deg, #06b6d4, #7c3aed); color: white; flex: 1;
|
||||
}
|
||||
.eid-nudge-btn--primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(6,182,212,0.3); }
|
||||
.eid-nudge-btn--dismiss {
|
||||
background: transparent; color: var(--rs-text-muted, #64748b);
|
||||
border: 1px solid var(--rs-border, #334155);
|
||||
}
|
||||
.eid-nudge-btn--dismiss:hover { color: var(--rs-text-secondary, #94a3b8); }
|
||||
@keyframes eid-nudge-in { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
|
||||
</style>
|
||||
<div class="eid-nudge-header">
|
||||
<span class="eid-nudge-icon">📱</span>
|
||||
<span class="eid-nudge-title">Link a second device</span>
|
||||
</div>
|
||||
<div class="eid-nudge-body">
|
||||
Sign in from your phone or tablet too — it also acts as a backup if you ever lose access to this device.
|
||||
</div>
|
||||
<div class="eid-nudge-actions">
|
||||
<button class="eid-nudge-btn eid-nudge-btn--primary" data-action="setup">Set up now</button>
|
||||
<button class="eid-nudge-btn eid-nudge-btn--dismiss" data-action="later">Later</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toast.querySelector('[data-action="setup"]')?.addEventListener("click", () => {
|
||||
toast.remove();
|
||||
this.showAccountModal({ openSection: "device" });
|
||||
});
|
||||
toast.querySelector('[data-action="later"]')?.addEventListener("click", () => {
|
||||
localStorage.setItem(NUDGE_KEY, String(Date.now()));
|
||||
toast.style.animation = "eid-nudge-in 0.3s ease-in reverse forwards";
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
});
|
||||
|
||||
document.body.appendChild(toast);
|
||||
} catch { /* offline */ }
|
||||
}
|
||||
|
||||
#onStorageChange = (e: StorageEvent) => {
|
||||
if (e.key === "encryptid_session" || e.key === PERSONAS_KEY) {
|
||||
this.#render();
|
||||
|
|
@ -980,7 +1063,7 @@ export class RStackIdentity extends HTMLElement {
|
|||
|
||||
// ── Account modal (consolidated) ──
|
||||
|
||||
showAccountModal(): void {
|
||||
showAccountModal(options?: { openSection?: string }): void {
|
||||
if (document.querySelector(".rstack-account-overlay")) return;
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "rstack-account-overlay";
|
||||
|
|
@ -988,7 +1071,7 @@ export class RStackIdentity extends HTMLElement {
|
|||
const session = getSession();
|
||||
if (!session) return;
|
||||
|
||||
let openSection: string | null = null;
|
||||
let openSection: string | null = options?.openSection ?? null;
|
||||
|
||||
// Account completion status
|
||||
let acctStatus: { email: boolean; emailAddress?: string | null; multiDevice: boolean; socialRecovery: boolean; guardianCount: number; credentialCount: number } | null = null;
|
||||
|
|
|
|||
Loading…
Reference in New Issue