eleven / messenger Public

home: empty-list passkey bridge + combine prove-step breathing room (#1139)

An anchor sign-in that resolves a fresh account lands on an empty list with
no hint that the person's spaces may live under their passkey β€” offer the
ordinary passkey door in place (atomic: nothing changes unless the ceremony
succeeds). Plus the combine screen's prove note gets the same 14px it was
missing above its button.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell e857b08eeb38274e458ed8b27b9479eb2cc17cc1 parent 00ab768
3 files changed, +48 −0
  • changelog/2026-07-19-bring-spaces-over.md +6 −0
  • home/web/pages/index.html +3 −0
  • home/web/static/home.js +39 −0
diff --git a/changelog/2026-07-19-bring-spaces-over.md b/changelog/2026-07-19-bring-spaces-over.md
new file mode 100644
index 0000000..2827384
--- /dev/null
+++ b/changelog/2026-07-19-bring-spaces-over.md
@@ -0,0 +1,6 @@
+πŸ€– Signed in with Apple or email and found an empty spaces list β€” but you
+know you have spaces? They probably live with your passkey. The empty list
+now offers the bridge: "Already have spaces? Sign in with your passkey to
+bring them over" runs your passkey right there and lands you back in your
+account. From Sign-in options you can then add Apple or email to it, so
+every way you sign in opens the same account.
diff --git a/home/web/pages/index.html b/home/web/pages/index.html
index 57c4956..0b99e89 100644
--- a/home/web/pages/index.html
+++ b/home/web/pages/index.html
@@ -324,6 +324,9 @@
/* The combine offer rides under whichever status line showed the 409. */
#combine-offer { margin-top: 10px; }
#combine-prove .anchor-note, #combine-vault #combine-vault-note { text-align: left; }
+ /* The prove note needs the same air above its button as the vault stage's
+ sub gets β€” without it the explainer sits flush on Continue with Apple. */
+ #combine-prove .anchor-note { margin-bottom: 14px; }
#combine-vault .create-sub { margin-bottom: 14px; }
/* Low-emphasis, full-width: Done (confirmation) and Cancel (archive). */
.wide-quiet {
diff --git a/home/web/static/home.js b/home/web/static/home.js
index d4b278c..aafce43 100644
--- a/home/web/static/home.js
+++ b/home/web/static/home.js
@@ -2061,6 +2061,11 @@ function showVault() {
box.append(emptyLine(archived.length
? "All your spaces are archived β€” open the archive to bring one back."
: "None (yet). Creating is invite-only for now."));
+ // Truly empty + signed in through an anchor: this may be the wrong-door
+ // landing β€” their spaces could live under an older passkey account.
+ const m = sessionMethod();
+ if (!archived.length && m && m !== "passkey" && window.PublicKeyCredential)
+ box.append(bringOverLine());
} else {
const servers = query ? filterServers(active, query) : active;
if (!servers.length) {
@@ -2205,6 +2210,40 @@ function emptyLine(text) {
return p;
}
+// Which method is holding THIS session up β€” the persisted session records it;
+// the last-signin marker covers a tab-only (unpersisted) session. "" when
+// unknowable.
+function sessionMethod() {
+ const s = persistedSession();
+ if (s && s.method) return s.method;
+ try { return localStorage.getItem("home_last_signin") || ""; } catch { return ""; }
+}
+
+// The wrong-door landing: an Apple/email sign-in that resolves a brand-new
+// account shows an empty list β€” but this person's spaces may live under the
+// passkey they used before anchors existed. Offer the bridge in place: the
+// ordinary passkey door, run from the signed-in view. signIn() touches no
+// state until the ceremony succeeds, so cancelling leaves this session
+// exactly as it was; success swaps to the passkey account, where Sign-in
+// options β†’ Add pulls this anchor over (the combine flow).
+function bringOverLine() {
+ const p = document.createElement("p");
+ p.className = "empty";
+ const b = document.createElement("button");
+ b.type = "button";
+ b.className = "passkey-link";
+ b.textContent = "Sign in with your passkey";
+ b.addEventListener("click", () => {
+ b.disabled = true;
+ vaultStatus("Confirming it’s you…", "busy");
+ signIn().then(() => vaultStatus(""), () => {
+ vaultStatus("That passkey didn’t confirm β€” nothing has changed.", "error");
+ }).finally(() => { b.disabled = false; setSignedOutBusy(false); setStatus(""); });
+ });
+ p.append("Already have spaces? ", b, " to bring them over.");
+ return p;
+}
+
function sectionHeading(text) {
const h = document.createElement("div");
h.className = "spaces-head";