web: a setup link from home doesn't ask you to sign in again (#1445)
Create a space from your signed-in Eleven page, tap "Set up your space", and the new space led with the signup-first card — asking you to sign in one tap after you signed in, to save a space home had already saved. offerPrejoin never looked at ?src=home, the marker home stamps on the handover link. Now it does, so a from-home setup link falls straight through to Eleven's setup conversation. The sign-in that link genuinely owes — adding this SPACE'S KEY to the account — is unchanged, at the conversation's keys-home step. Claude-Session: https://claude.ai/code/session_01GUA1EJFYwgr2nZNVBRFf8k
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
5 files changed,
+33
−6
changelog/2026-08-28-no-second-signin-on-setup.md+3 −0docs/superpowers/specs/2026-07-31-signup-first-join-design.md+5 −0test/prejoin.test.mjs+9 −0web/static/join.js+7 −4web/static/prejoin.js+9 −2
diff --git a/changelog/2026-08-28-no-second-signin-on-setup.md b/changelog/2026-08-28-no-second-signin-on-setup.md| new file mode 100644 |
| index 0000000..de212dc |
| --- /dev/null |
| +++ b/changelog/2026-08-28-no-second-signin-on-setup.md |
| @@ -0,0 +1,3 @@ |
| +🤖 Making a new space no longer asks you to sign in twice. Come from your |
| +Eleven page and your new space opens straight into setting it up — the space |
| +is already saved to your account, so there's nothing to sign into again. |
diff --git a/docs/superpowers/specs/2026-07-31-signup-first-join-design.md b/docs/superpowers/specs/2026-07-31-signup-first-join-design.md| index fb96790..0076012 100644 |
| --- a/docs/superpowers/specs/2026-07-31-signup-first-join-design.md |
| +++ b/docs/superpowers/specs/2026-07-31-signup-first-join-design.md |
| @@ -48,6 +48,11 @@ These paths fall through to today's behaviour untouched: |
| - Dead/consumed invites — the existing error card (with its sign-in door). |
| - Instances with no home server configured (`__HOME_ORIGIN__` empty — |
| self-hosters). |
| +- 🤖 Setup links opened from home's create-a-space flow (`?src=home`): that |
| + person signed in at home one tap ago, and home has already saved the new |
| + space to their account. The sign-in they still owe — adding this space's |
| + KEY to that account — is the setup conversation's `keys-home` step, at the |
| + end. |
| ## 3. The home-side pre-join intent |
diff --git a/test/prejoin.test.mjs b/test/prejoin.test.mjs| index 6a55b4b..4cd3989 100644 |
| --- a/test/prejoin.test.mjs |
| +++ b/test/prejoin.test.mjs |
| @@ -24,6 +24,15 @@ test("offerPrejoin: never for established or unsupported contexts", () => { |
| assert.equal(offerPrejoin({ ...base, signedUp: true }), false); |
| }); |
| +// The create-a-space journey: home hands over /join/setup?src=home moments |
| +// after a sign-in there (and having already saved the space to that account). |
| +// Leading with the sign-in door again reads as the flow forgetting. |
| +test("offerPrejoin: a setup link from a signed-in home page never leads with sign-in", () => { |
| + assert.equal(offerPrejoin({ ...base, kind: "setup", fromHome: true }), false); |
| + assert.equal(offerPrejoin({ ...base, kind: "join", fromHome: true }), false); |
| + assert.equal(offerPrejoin({ ...base, kind: "setup", fromHome: false }), true); |
| +}); |
| + |
| test("round trip: stash then read back on a genuine return", () => { |
| const now = 1_000_000; |
| const stored = prejoinStash({ frag: "e1.SECRET~Ann~bkey", nonce: "n0nce", now }); |
diff --git a/web/static/join.js b/web/static/join.js| index 5c8a75e..af479e9 100644 |
| --- a/web/static/join.js |
| +++ b/web/static/join.js |
| @@ -68,10 +68,12 @@ const prejoin = (() => { |
| const { T, name: fragName, b: fragB } = parseInviteFragment(location.hash.slice(1) || prejoin.frag); |
| // ?src=home marks a link opened from the person's Eleven sign-in page (the |
| -// create-a-space flow). Remember it on this origin so the setup conversation's |
| -// key step can say "add to your passkey" instead of pitching passkeys from |
| -// scratch. A hint only — nothing downstream trusts it for access. |
| -if (new URLSearchParams(location.search).get("src") === "home") { |
| +// create-a-space flow). Two jobs, neither of them access: it stands the |
| +// signup-first lead card down (they signed in one tap ago — offerPrejoin), and |
| +// it's remembered on this origin so the setup conversation's key step can say |
| +// "add to your account" instead of pitching a sign-in from scratch. |
| +const fromHome = new URLSearchParams(location.search).get("src") === "home"; |
| +if (fromHome) { |
| try { localStorage.setItem("lchat_from_home", "1"); } catch { /* storage denied: the generic copy shows */ } |
| } |
| @@ -358,6 +360,7 @@ function showWelcome(kind, onContinue, spaceDesc = "", opts = {}) { |
| embed: !!window.__lchatEmbed, |
| native: document.documentElement.classList.contains("native"), |
| signedUp: prejoin.signedUp, |
| + fromHome, // came straight from a signed-in home page — don't ask twice |
| })) { |
| return showPrejoin(kind, onContinue, spaceDesc); |
| } |
diff --git a/web/static/prejoin.js b/web/static/prejoin.js| index a8012a5..2b4b547 100644 |
| --- a/web/static/prejoin.js |
| +++ b/web/static/prejoin.js |
| @@ -15,8 +15,15 @@ const MAX_AGE_MS = 60 * 60 * 1000; |
| // device links and adopts already have an identity), only where a home server |
| // exists to sign into, and never in the native embed/wrapper (the apps own |
| // their sign-in). signedUp means the round trip already happened. |
| -export function offerPrejoin({ kind, homeOrigin, embed, native, signedUp }) { |
| - if (embed || native || signedUp) return false; |
| +// |
| +// fromHome (the setup link's ?src=home) is the other established identity: |
| +// this person just created the space FROM their signed-in Eleven page, which |
| +// already saved it to their account — asking them to sign in again, one tap |
| +// after signing in, is the flow contradicting itself. They meet the sign-in |
| +// once more at the end, where it belongs: the setup conversation's keys-home |
| +// step, adding this space's key to the account they already have. |
| +export function offerPrejoin({ kind, homeOrigin, embed, native, signedUp, fromHome }) { |
| + if (embed || native || signedUp || fromHome) return false; |
| if (kind !== "join" && kind !== "setup") return false; |
| return typeof homeOrigin === "string" && homeOrigin.startsWith("http"); |
| } |