feat(identity): replace device nudge toast with QR code for mobile linking
Instead of a "Set up now" button, the device nudge now generates a device link token and displays a scannable QR code + copyable link URL directly in the toast, making it easy to link a phone or tablet. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
df8631360e
commit
13a7e44e24
|
|
@ -420,62 +420,116 @@ export class RStackIdentity extends HTMLElement {
|
|||
const status = await res.json();
|
||||
if (status.multiDevice) return; // already has 2+ devices
|
||||
|
||||
// Show a toast nudge
|
||||
// Show a toast nudge with QR code
|
||||
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);
|
||||
});
|
||||
let linkUrl = "";
|
||||
let linkError = "";
|
||||
|
||||
const renderNudge = () => {
|
||||
const qrHTML = linkUrl
|
||||
? `<div class="eid-nudge-qr">
|
||||
<img src="https://api.qrserver.com/v1/create-qr-code/?size=160x160&data=${encodeURIComponent(linkUrl)}&format=png&margin=6" width="160" height="160" alt="Scan to link device" />
|
||||
</div>
|
||||
<div class="eid-nudge-link-row">
|
||||
<input class="eid-nudge-link-input" type="text" readonly value="${linkUrl}" />
|
||||
<button class="eid-nudge-btn eid-nudge-btn--copy" data-action="copy">Copy</button>
|
||||
</div>
|
||||
<div class="eid-nudge-expire">Expires in 10 minutes</div>`
|
||||
: linkError
|
||||
? `<div class="eid-nudge-error">${linkError}</div>`
|
||||
: `<div class="eid-nudge-loading"><span class="eid-nudge-spinner"></span> Generating link…</div>`;
|
||||
|
||||
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-qr { text-align: center; margin-bottom: 0.5rem; }
|
||||
.eid-nudge-qr img { border-radius: 10px; background: #fff; padding: 4px; }
|
||||
.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);
|
||||
color: var(--rs-text-secondary, #94a3b8); min-width: 0; outline: none;
|
||||
}
|
||||
.eid-nudge-btn--copy {
|
||||
padding: 5px 10px; border-radius: 6px; border: 1px solid var(--rs-border, #334155);
|
||||
background: transparent; color: var(--rs-text-secondary, #94a3b8);
|
||||
font-size: 0.75rem; font-weight: 600; cursor: pointer; white-space: nowrap;
|
||||
}
|
||||
.eid-nudge-btn--copy:hover { color: var(--rs-text-primary, #e2e8f0); }
|
||||
.eid-nudge-expire { font-size: 0.75rem; color: var(--rs-text-muted, #64748b); text-align: center; margin-bottom: 0.5rem; }
|
||||
.eid-nudge-loading { text-align: center; padding: 1.5rem 0; color: var(--rs-text-secondary, #94a3b8); font-size: 0.85rem; }
|
||||
.eid-nudge-error { text-align: center; padding: 0.75rem 0; color: #f87171; font-size: 0.85rem; }
|
||||
.eid-nudge-spinner {
|
||||
display: inline-block; width: 14px; height: 14px; border: 2px solid rgba(148,163,184,0.3);
|
||||
border-top-color: #94a3b8; border-radius: 50%; animation: eid-spin 0.6s linear infinite;
|
||||
vertical-align: middle; margin-right: 6px;
|
||||
}
|
||||
.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--dismiss {
|
||||
background: transparent; color: var(--rs-text-muted, #64748b);
|
||||
border: 1px solid var(--rs-border, #334155); flex: 1; text-align: center;
|
||||
}
|
||||
.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); } }
|
||||
@keyframes eid-spin { to { transform: rotate(360deg); } }
|
||||
</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">
|
||||
Scan this QR code on your phone or tablet to add a passkey backup.
|
||||
</div>
|
||||
${qrHTML}
|
||||
<div class="eid-nudge-actions">
|
||||
<button class="eid-nudge-btn eid-nudge-btn--dismiss" data-action="later">Dismiss</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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);
|
||||
});
|
||||
toast.querySelector('[data-action="copy"]')?.addEventListener("click", () => {
|
||||
navigator.clipboard.writeText(linkUrl).catch(() => {});
|
||||
const btn = toast.querySelector('[data-action="copy"]') as HTMLButtonElement;
|
||||
if (btn) { btn.textContent = "Copied!"; setTimeout(() => { btn.textContent = "Copy"; }, 2000); }
|
||||
});
|
||||
};
|
||||
|
||||
document.body.appendChild(toast);
|
||||
renderNudge(); // show loading state
|
||||
|
||||
// Generate device link
|
||||
try {
|
||||
const linkRes = await fetch(`${ENCRYPTID_URL}/api/device-link/start`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${getAccessToken()}` },
|
||||
});
|
||||
const linkData = await linkRes.json();
|
||||
if (!linkRes.ok || linkData.error) throw new Error(linkData.error || "Failed");
|
||||
linkUrl = linkData.linkUrl;
|
||||
} catch {
|
||||
linkError = "Could not generate link. Try My Account → Devices.";
|
||||
}
|
||||
renderNudge(); // show QR or error
|
||||
} catch { /* offline */ }
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue