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();
|
const status = await res.json();
|
||||||
if (status.multiDevice) return; // already has 2+ devices
|
if (status.multiDevice) return; // already has 2+ devices
|
||||||
|
|
||||||
// Show a toast nudge
|
// Show a toast nudge with QR code
|
||||||
const toast = document.createElement("div");
|
const toast = document.createElement("div");
|
||||||
toast.className = "eid-device-nudge";
|
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", () => {
|
let linkUrl = "";
|
||||||
toast.remove();
|
let linkError = "";
|
||||||
this.showAccountModal({ openSection: "device" });
|
|
||||||
});
|
const renderNudge = () => {
|
||||||
toast.querySelector('[data-action="later"]')?.addEventListener("click", () => {
|
const qrHTML = linkUrl
|
||||||
localStorage.setItem(NUDGE_KEY, String(Date.now()));
|
? `<div class="eid-nudge-qr">
|
||||||
toast.style.animation = "eid-nudge-in 0.3s ease-in reverse forwards";
|
<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" />
|
||||||
setTimeout(() => toast.remove(), 300);
|
</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);
|
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 */ }
|
} catch { /* offline */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue