feat(encryptid): post-signup prompt recommending second device linking
After registration, users now see a welcome modal that prominently recommends linking a second device via QR code before entering their space. Provides backup access and cross-device sign-in awareness. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
772e5e4352
commit
a7eda3c53f
|
|
@ -810,8 +810,8 @@ export class RStackIdentity extends HTMLElement {
|
|||
this.#render();
|
||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
|
||||
callbacks?.onSuccess?.();
|
||||
// Auto-redirect to personal space
|
||||
autoResolveSpace(data.token, username);
|
||||
// Show post-signup prompt recommending second device before redirecting
|
||||
this.#showPostSignupPrompt(data.token, username);
|
||||
} catch (err: any) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = "🔐 Create Passkey";
|
||||
|
|
@ -861,6 +861,123 @@ export class RStackIdentity extends HTMLElement {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Post-signup onboarding prompt ──
|
||||
|
||||
#showPostSignupPrompt(token: string, username: string): void {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "rstack-auth-overlay";
|
||||
|
||||
const goToSpace = () => {
|
||||
overlay.remove();
|
||||
autoResolveSpace(token, username);
|
||||
};
|
||||
|
||||
let step: "welcome" | "linking" | "done" = "welcome";
|
||||
let linkUrl = "";
|
||||
|
||||
const render = () => {
|
||||
if (step === "welcome") {
|
||||
const qrHint = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent)
|
||||
? "You can link your laptop or another phone."
|
||||
: "Grab your phone or tablet and scan the code.";
|
||||
overlay.innerHTML = `
|
||||
<style>${MODAL_STYLES}${ONBOARDING_STYLES}</style>
|
||||
<div class="auth-modal onboarding-modal">
|
||||
<h2>Welcome to rSpace!</h2>
|
||||
<p style="margin-bottom:0.75rem">Your account <strong>${username}</strong> is ready.</p>
|
||||
|
||||
<div class="onboarding-card onboarding-card--primary">
|
||||
<div class="onboarding-card-icon">📱</div>
|
||||
<div class="onboarding-card-content">
|
||||
<div class="onboarding-card-title">Link a Second Device</div>
|
||||
<div class="onboarding-card-desc">
|
||||
Add a passkey on your phone or tablet so you can sign in from anywhere.
|
||||
This also serves as a backup if you lose access to this device.
|
||||
</div>
|
||||
<div class="onboarding-card-hint">${qrHint}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn--primary" style="width:100%;margin-top:0.75rem" data-action="link-device">📱 Link Another Device</button>
|
||||
|
||||
<button class="btn btn--outline" style="width:100%;margin-top:0.5rem" data-action="skip">Continue to my space</button>
|
||||
<p class="onboarding-later-hint">You can always link devices later from My Account.</p>
|
||||
</div>`;
|
||||
} else if (step === "linking") {
|
||||
const qrSrc = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(linkUrl)}&format=png&margin=8`;
|
||||
overlay.innerHTML = `
|
||||
<style>${MODAL_STYLES}${ONBOARDING_STYLES}</style>
|
||||
<div class="auth-modal onboarding-modal">
|
||||
<h2>Link Another Device</h2>
|
||||
<p style="margin-bottom:0.5rem">Scan this QR code on your phone or tablet to add a passkey.</p>
|
||||
<div class="qr-container">
|
||||
<img src="${qrSrc}" width="200" height="200" alt="QR Code" style="border-radius:12px;background:#fff;padding:4px" />
|
||||
</div>
|
||||
<div class="link-copy-row">
|
||||
<input class="input" id="onboarding-link-url" type="text" readonly value="${linkUrl}" style="font-size:0.8rem;margin-bottom:0" />
|
||||
<button class="btn btn--secondary btn--small" data-action="copy-link" style="flex:none;white-space:nowrap">Copy</button>
|
||||
</div>
|
||||
<p class="onboarding-expire-hint">Link expires in 10 minutes</p>
|
||||
<div class="actions" style="margin-top:1rem">
|
||||
<button class="btn btn--secondary" data-action="back">Back</button>
|
||||
<button class="btn btn--primary" data-action="done">Continue to my space</button>
|
||||
</div>
|
||||
</div>`;
|
||||
} else {
|
||||
overlay.innerHTML = `
|
||||
<style>${MODAL_STYLES}${ONBOARDING_STYLES}</style>
|
||||
<div class="auth-modal onboarding-modal">
|
||||
<div style="font-size:2.5rem;margin-bottom:0.5rem">✅</div>
|
||||
<h2>Device Linked!</h2>
|
||||
<p>You can now sign in from your other device too.</p>
|
||||
<button class="btn btn--primary" style="width:100%;margin-top:1rem" data-action="done">Continue to my space</button>
|
||||
</div>`;
|
||||
}
|
||||
attachListeners();
|
||||
};
|
||||
|
||||
const attachListeners = () => {
|
||||
overlay.querySelector('[data-action="skip"]')?.addEventListener("click", goToSpace);
|
||||
overlay.querySelector('[data-action="done"]')?.addEventListener("click", goToSpace);
|
||||
overlay.querySelector('[data-action="back"]')?.addEventListener("click", () => {
|
||||
step = "welcome";
|
||||
render();
|
||||
});
|
||||
overlay.querySelector('[data-action="copy-link"]')?.addEventListener("click", () => {
|
||||
const input = overlay.querySelector("#onboarding-link-url") as HTMLInputElement;
|
||||
navigator.clipboard.writeText(input.value).catch(() => {});
|
||||
const btn = overlay.querySelector('[data-action="copy-link"]') as HTMLButtonElement;
|
||||
btn.textContent = "Copied!";
|
||||
setTimeout(() => { btn.textContent = "Copy"; }, 2000);
|
||||
});
|
||||
overlay.querySelector('[data-action="link-device"]')?.addEventListener("click", async () => {
|
||||
const btn = overlay.querySelector('[data-action="link-device"]') as HTMLButtonElement;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Generating link...';
|
||||
try {
|
||||
const res = await fetch(`${ENCRYPTID_URL}/api/device-link/start`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${getAccessToken()}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok || data.error) throw new Error(data.error || "Failed to generate link");
|
||||
linkUrl = data.linkUrl;
|
||||
step = "linking";
|
||||
render();
|
||||
} catch (e: any) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = "📱 Link Another Device";
|
||||
const errEl = document.createElement("div");
|
||||
errEl.className = "error";
|
||||
errEl.textContent = e.message;
|
||||
btn.parentElement?.appendChild(errEl);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
render();
|
||||
}
|
||||
|
||||
// ── Account modal (consolidated) ──
|
||||
|
||||
showAccountModal(): void {
|
||||
|
|
@ -2100,6 +2217,38 @@ const ACCOUNT_MODAL_STYLES = `
|
|||
}
|
||||
`;
|
||||
|
||||
const ONBOARDING_STYLES = `
|
||||
.onboarding-modal { max-width: 440px; }
|
||||
.onboarding-card {
|
||||
border: 1px solid var(--rs-border); border-radius: 12px;
|
||||
padding: 1rem 1.25rem; display: flex; gap: 1rem; align-items: flex-start;
|
||||
text-align: left; margin-top: 0.75rem; transition: border-color 0.2s;
|
||||
}
|
||||
.onboarding-card--primary {
|
||||
border-color: rgba(6,182,212,0.4);
|
||||
background: linear-gradient(135deg, rgba(6,182,212,0.06), rgba(124,58,237,0.06));
|
||||
}
|
||||
.onboarding-card-icon { font-size: 2rem; flex-shrink: 0; margin-top: 2px; }
|
||||
.onboarding-card-content { flex: 1; min-width: 0; }
|
||||
.onboarding-card-title {
|
||||
font-size: 1rem; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 4px;
|
||||
}
|
||||
.onboarding-card-desc {
|
||||
font-size: 0.85rem; color: var(--rs-text-secondary); line-height: 1.5;
|
||||
}
|
||||
.onboarding-card-hint {
|
||||
font-size: 0.8rem; color: #06b6d4; margin-top: 6px; font-weight: 500;
|
||||
}
|
||||
.onboarding-later-hint {
|
||||
font-size: 0.75rem; color: var(--rs-text-muted); margin-top: 0.5rem;
|
||||
}
|
||||
.qr-container { text-align: center; margin: 0.75rem 0; }
|
||||
.link-copy-row { display: flex; gap: 8px; align-items: stretch; }
|
||||
.onboarding-expire-hint {
|
||||
font-size: 0.75rem; color: var(--rs-text-muted); text-align: center; margin-top: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const SPACES_STYLES = `
|
||||
.rstack-spaces-overlay {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,0.6);
|
||||
|
|
|
|||
Loading…
Reference in New Issue