eleven / messenger Public

web: Eleven just says Sign in — no passkey/backup talk, one parting reminder (#713)

The welcome and setup conversations drop every mention of passkeys and
backups: the ask is 'Sign in with Eleven' (face or fingerprint, one tap),
with the lock-and-key framing carrying the stakes. Record labels and chips
follow ('Signed in — your access is safe', 'Skipped signing in for now');
internal record VALUES stay 'passkey'/'skipped' for wire compat.

Choosing 'Not now' now earns one honest parting reminder in the finish
bubble — plain consequences, no mechanism talk — with a live 'Sign in' chip
right there (the completion poll picks up a sign-in even after the flow
ends). Native flow twins regenerated; web+Swift test pins updated.

Verified: 17/17 flow tests, full npm suite, and the Playwright E2E (real
ceremony) on both the desktop-redirect and stay-mode legs with the new copy.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 80347428901bfbc3cc38a35be6a2770c6e055dee parent 154e3b6
7 files changed, +141 −85
  • changelog/2026-07-11-sign-in-words.md +1 −0
  • ios/LChatCore/Sources/LChatCore/Flows/setup.json +35 −13
  • ios/LChatCore/Sources/LChatCore/Flows/welcome.json +23 −11
  • ios/LChatCore/Tests/LChatCoreTests/FlowEngineTests.swift +3 −3
  • test/flow.test.mjs +26 −26
  • web/static/apps/setup.flow.js +29 −18
  • web/static/apps/welcome.flow.js +24 −14
diff --git a/changelog/2026-07-11-sign-in-words.md b/changelog/2026-07-11-sign-in-words.md
new file mode 100644
index 0000000..34742ec
--- /dev/null
+++ b/changelog/2026-07-11-sign-in-words.md
@@ -0,0 +1 @@
+🤖 Eleven now just says "Sign in". The welcome and setup conversations stopped talking about passkeys and backups — the ask is one plain thing: sign in with Eleven so your access is safe. And if you choose "Not now", Eleven gives you one honest parting reminder at the end — with a Sign in button right there — instead of leaving it unsaid.
diff --git a/ios/LChatCore/Sources/LChatCore/Flows/setup.json b/ios/LChatCore/Sources/LChatCore/Flows/setup.json
index 17aad8f..8b6944b 100644
--- a/ios/LChatCore/Sources/LChatCore/Flows/setup.json
+++ b/ios/LChatCore/Sources/LChatCore/Flows/setup.json
@@ -12,6 +12,12 @@
"done.keys"
]
},
+ "resumeUnless": {
+ "all": [
+ "bridge.enrolled",
+ "!done.keys"
+ ]
+ },
"resumeSay": "Welcome back — let's pick up where we left off.",
"steps": [
{
@@ -137,6 +143,10 @@
{
"label": "Skip for now",
"spent": "Skipped",
+ "record": {
+ "label": "skipped the contact email",
+ "value": "skipped"
+ },
"goto": "photo"
}
],
@@ -186,6 +196,10 @@
"label": "Skip for now",
"spent": "Skipped",
"cancelInput": true,
+ "record": {
+ "label": "skipped the space photo",
+ "value": "skipped"
+ },
"goto": "invites"
}
],
@@ -309,10 +323,10 @@
"vars.backedUp": true
},
"recordIfNew": {
- "label": "Saved your credentials securely with a passkey",
+ "label": "Signed in — your space's key is safe",
"value": "passkey"
},
- "sayIfNew": "Your key is safely backed up behind your passkey. ✅",
+ "sayIfNew": "You're signed in — your space's key is safe. ✅",
"goto": "finish"
},
{
@@ -328,7 +342,7 @@
"text": "Now the most important thing of all. 🔑\n\nEverything here is locked with a key that exists only on your devices. There's no password reset and no support desk — not even whoever runs the server can read your messages or recover that key. That's not a flaw; it's the whole point.\n\nBut it means: right now, this browser holds the only key to your space. If it's lost — cleared, broken, stolen — your space is gone with it, and nobody can get it back."
},
{
- "text": "Sign in with Eleven and your key stays safe with your passkey — one tap, and you'll be brought straight back."
+ "text": "Sign in with Eleven and your key stays safe — one tap, your face or fingerprint, and you'll be brought straight back."
}
],
"chips": [
@@ -360,10 +374,10 @@
"vars.backedUp": true
},
"recordIfNew": {
- "label": "Saved your credentials securely with a passkey",
+ "label": "Signed in — your space's key is safe",
"value": "passkey"
},
- "sayIfNew": "Your key is safely backed up behind your passkey. ✅",
+ "sayIfNew": "You're signed in — your space's key is safe. ✅",
"goto": "finish"
}
],
@@ -375,7 +389,7 @@
"text": "Last thing — the key. 🔑\n\nThis space is locked with a key that was just created here, in this browser. Your Eleven sign-in doesn't hold it yet — so if this browser is lost, the space goes with it."
},
{
- "text": "Sign in and it's added to your passkey — one tap, and you'll be brought straight back."
+ "text": "Sign in and it's added to your account — one tap, and you'll be brought straight back."
}
],
"chips": [
@@ -416,8 +430,8 @@
"step": "keys",
"label": {
"if": "bridge.enrolled",
- "then": "Saved your credentials securely with a passkey",
- "else": "Saved your credentials with a passkey (in your browser)"
+ "then": "Signed in — your space's key is safe",
+ "else": "Signed in (in this browser)"
},
"value": "passkey"
},
@@ -431,7 +445,7 @@
"spent": "Skipped",
"record": {
"step": "keys",
- "label": "Skipped the credentials backup for now",
+ "label": "Skipped signing in for now",
"value": "skipped"
},
"set": {
@@ -449,12 +463,12 @@
},
"bubbles": [
{
- "text": "I have to be straight with you: without a backup, one lost or cleared browser means this space and everything in it is gone — permanently, for everyone, and no one can help. I'll remind you again in a day or so."
+ "text": "I have to be straight with you: if you don't sign in, one lost or cleared browser means this space and everything in it is gone — permanently, for everyone, and no one can help. I'll remind you again in a day or so."
}
],
"chips": [
{
- "label": "Go back and save it",
+ "label": "Go back and sign in",
"advancing": false,
"effect": {
"verb": "savePasskey"
@@ -470,7 +484,7 @@
"spent": "Understood",
"record": {
"step": "keys",
- "label": "Skipped the credentials backup for now",
+ "label": "Skipped signing in for now",
"value": "skipped"
},
"set": {
@@ -502,7 +516,7 @@
"Everything we just set up lives in your Admin panel — invite links, the space's name and photo, who can invite. Type `/admin` anywhere to open it; it's yours alone as this space's admin.",
{
"if": "!vars.backedUp",
- "text": "\n\nAnd do come back to that key backup soon."
+ "text": "\n\nOne more reminder before you go: you haven't signed in, so this browser holds the only key to this space. If it's lost or cleared, the space is gone with it — for everyone. Sign in any time and it's safe."
}
]
},
@@ -511,6 +525,14 @@
}
],
"chips": [
+ {
+ "if": "!vars.backedUp",
+ "label": "Sign in",
+ "advancing": false,
+ "effect": {
+ "verb": "savePasskey"
+ }
+ },
{
"label": "Open admin",
"advancing": false,
diff --git a/ios/LChatCore/Sources/LChatCore/Flows/welcome.json b/ios/LChatCore/Sources/LChatCore/Flows/welcome.json
index e8bc505..1381ec7 100644
--- a/ios/LChatCore/Sources/LChatCore/Flows/welcome.json
+++ b/ios/LChatCore/Sources/LChatCore/Flows/welcome.json
@@ -69,10 +69,10 @@
]
},
"recordIfNew": {
- "label": "Saved your access securely with a passkey",
+ "label": "Signed in — your access is safe",
"value": "passkey"
},
- "sayIfNew": "Great — welcome back. 🔒 Now that your access is secure, let's continue.",
+ "sayIfNew": "Great — welcome back. 🔒 You're signed in, so your access is safe. Let's continue.",
"goto": "name"
}
],
@@ -81,12 +81,12 @@
},
"bubbles": [
{
- "text": "Before anything else, let's make sure you can't lose your way in. 🔑\n\nYour access to this space lives in a key that exists only in this browser — there's no username or password, and nobody can reset it for you. If this browser's data is cleared, your access goes with it.\n\nShall we save it behind a passkey now? One tap — you'll be brought straight back and we'll carry on."
+ "text": "Before anything else, let's make sure you can't lose your way in. 🔑\n\nYour access to this space lives in a key that exists only in this browser — there's no username or password, and nobody can reset it for you. If this browser's data is cleared, your access goes with it.\n\nSign in with Eleven and your access is safe — one tap, your face or fingerprint, and you'll be brought straight back so we can carry on."
}
],
"chips": [
{
- "label": "Save my access (recommended)",
+ "label": "Sign in (recommended)",
"advancing": false,
"effect": {
"verb": "savePasskey"
@@ -104,7 +104,7 @@
"verb": "deferBackupReminders"
},
"record": {
- "label": "Skipped the access backup for now",
+ "label": "Skipped signing in for now",
"value": "skipped"
},
"ack": "No problem — I'll nudge you again in a day or so.",
@@ -120,7 +120,7 @@
},
"bubbles": [
{
- "text": "I'll wait here while you finish saving the passkey. When you're done, tap Continue."
+ "text": "I'll wait here while you finish signing in. When you're done, tap Continue."
}
],
"chips": [
@@ -130,12 +130,12 @@
"step": "access",
"label": {
"if": "bridge.enrolled",
- "then": "Saved your access securely with a passkey",
- "else": "Saved your access with a passkey (in your browser)"
+ "then": "Signed in — your access is safe",
+ "else": "Signed in (in this browser)"
},
"value": "passkey"
},
- "ack": "Perfect — that's saved. 🔒",
+ "ack": "Perfect — you're all set. 🔒",
"goto": "name"
},
{
@@ -146,7 +146,7 @@
},
"record": {
"step": "access",
- "label": "Skipped the access backup for now",
+ "label": "Skipped signing in for now",
"value": "skipped"
},
"ack": "No problem — I'll nudge you again in a day or so.",
@@ -236,6 +236,10 @@
"set": {
"vars.skippedPhoto": true
},
+ "record": {
+ "label": "skipped the profile photo",
+ "value": "skipped"
+ },
"goto": "finish"
}
],
@@ -316,7 +320,7 @@
"That's it — you're all set. 🎉 Enjoy {space.name|this space}!",
{
"if": "!accessBackedUp",
- "text": "\n\nAnd do come back to that access backup soon."
+ "text": "\n\nOne more reminder before you go: you haven't signed in yet, so your access to this space lives only in this browser — if it's cleared or lost, there's no way back in. Sign in any time and you're safe."
}
],
"key": "q-finish"
@@ -326,6 +330,14 @@
}
],
"chips": [
+ {
+ "if": "!accessBackedUp",
+ "label": "Sign in",
+ "advancing": false,
+ "effect": {
+ "verb": "savePasskey"
+ }
+ },
{
"if": {
"all": [
diff --git a/ios/LChatCore/Tests/LChatCoreTests/FlowEngineTests.swift b/ios/LChatCore/Tests/LChatCoreTests/FlowEngineTests.swift
index 6938442..8d36bbf 100644
--- a/ios/LChatCore/Tests/LChatCoreTests/FlowEngineTests.swift
+++ b/ios/LChatCore/Tests/LChatCoreTests/FlowEngineTests.swift
@@ -144,7 +144,7 @@ final class FlowEngineTests: XCTestCase {
XCTAssertEqual(io.records.last?.value, "done")
XCTAssertTrue(io.verbLog.contains { $0.0 == "markSetupComplete" })
XCTAssertFalse(io.verbLog.contains { $0.0 == "deferBackupReminders" })
- XCTAssertFalse(io.transcript.contains { $0.text.contains("come back to that key backup") })
+ XCTAssertFalse(io.transcript.contains { $0.text.contains("One more reminder") })
// The tour stays web-only; Admin hops natively now.
let allChips = io.transcript.flatMap { $0.chips ?? [] }.map(\.label)
XCTAssertFalse(allChips.contains("Take the tour"))
@@ -189,7 +189,7 @@ final class FlowEngineTests: XCTestCase {
// Greeting names the inviter; the passkey ask never renders; access
// records the native reason; the name question is armed.
XCTAssertTrue(io.transcript.first?.text.contains("Ciara invited you to Test Space") ?? false)
- XCTAssertFalse(io.transcript.contains { $0.text.contains("Shall we save it behind a passkey") })
+ XCTAssertFalse(io.transcript.contains { $0.text.contains("Sign in with Eleven and your access is safe") })
let access = io.records.first
XCTAssertEqual(access?.step, "access")
XCTAssertEqual(access?.value, "native")
@@ -204,7 +204,7 @@ final class FlowEngineTests: XCTestCase {
// Finish: no backup nag (access is the sign-in), completion recorded.
let fin = io.transcript.first { $0.extra.key == "q-finish" }
XCTAssertNotNil(fin)
- XCTAssertFalse(fin?.text.contains("access backup") ?? true)
+ XCTAssertFalse(fin?.text.contains("One more reminder") ?? true)
XCTAssertEqual(io.records.last?.step, "finish")
XCTAssertEqual(io.records.last?.value, "done")
XCTAssertTrue(io.ended == false) // onEnd fires on env, not io — checked below
diff --git a/test/flow.test.mjs b/test/flow.test.mjs
index 7071c11..80dcc57 100644
--- a/test/flow.test.mjs
+++ b/test/flow.test.mjs
@@ -204,14 +204,14 @@ await test("setup: the full first-run transcript, happy path with detours", asyn
// They finish the ceremony elsewhere; Continue records the verified label.
h.enrolled = true;
await h.tap("Continue");
- assert.deepEqual(h.records.find((r) => r.step === "keys"), { step: "keys", label: "Saved your credentials securely with a passkey", value: "passkey" });
+ assert.deepEqual(h.records.find((r) => r.step === "keys"), { step: "keys", label: "Signed in — your space's key is safe", value: "passkey" });
assert.deepEqual(h.records.at(-1), { step: "finish", label: "Your space is ready", value: "done" }, "finish persists its completion marker");
// Finish: complete marked, no reminder deferral (backed up), no nag line.
assert.ok(h.verbLog.find((v) => v[0] === "markSetupComplete"));
assert.equal(h.verbLog.some((v) => v[0] === "deferBackupReminders"), false);
const fin = h.transcript.find((t) => /Team Awesome is ready! 🎉/.test(t.text));
- assert.equal(/come back to that key backup/.test(fin.text), false);
+ assert.equal(/One more reminder/.test(fin.text), false);
await h.tap("Open admin");
assert.ok(h.verbLog.find((v) => v[0] === "openAdmin"));
assert.ok(h.ended, "flow signalled completion");
@@ -244,13 +244,13 @@ await test("setup: resumed run — skips, Keep chip, skip-everything path", asyn
await h.tap("Not now");
assert.match(h.lastText(), /I have to be straight with you/);
await h.tap("I understand the risk");
- assert.deepEqual(h.records.find((r) => r.step === "keys"), { step: "keys", label: "Skipped the credentials backup for now", value: "skipped" });
+ assert.deepEqual(h.records.find((r) => r.step === "keys"), { step: "keys", label: "Skipped signing in for now", value: "skipped" });
assert.deepEqual(h.records.at(-1), { step: "finish", label: "Your space is ready", value: "done" });
// Finish without a backup: reminders deferred, the nag line present.
assert.ok(h.verbLog.find((v) => v[0] === "deferBackupReminders"));
assert.ok(h.verbLog.find((v) => v[0] === "markSetupComplete"));
- assert.ok(h.transcript.find((t) => /come back to that key backup soon/.test(t.text)));
+ assert.ok(h.transcript.find((t) => /One more reminder before you go/.test(t.text)));
assert.ok(h.transcript.find((t) => /Old Name is ready! 🎉/.test(t.text)));
});
@@ -262,8 +262,8 @@ await test("setup: already enrolled — keys short-circuits with the once-only a
});
runFlow(SETUP_FLOW, h.env, h.io);
await tick();
- assert.deepEqual(h.records[0], { step: "keys", label: "Saved your credentials securely with a passkey", value: "passkey" });
- assert.ok(h.transcript.find((t) => /safely backed up behind your passkey. ✅/.test(t.text)));
+ assert.deepEqual(h.records[0], { step: "keys", label: "Signed in — your space's key is safe", value: "passkey" });
+ assert.ok(h.transcript.find((t) => /your space's key is safe. ✅/.test(t.text)));
assert.equal(h.verbLog.some((v) => v[0] === "deferBackupReminders"), false);
assert.ok(h.verbLog.find((v) => v[0] === "markSetupComplete"));
@@ -273,7 +273,7 @@ await test("setup: already enrolled — keys short-circuits with the once-only a
await tick();
// No duplicate keys record — only finish stamping its completion marker.
assert.deepEqual(h2.records, [{ step: "finish", label: "Your space is ready", value: "done" }]);
- assert.equal(h2.transcript.some((t) => /safely backed up/.test(t.text)), false);
+ assert.equal(h2.transcript.some((t) => /key is safe. ✅/.test(t.text)), false);
});
await test("setup: from-home key step + settle auto-detects the enrolment return", async () => {
@@ -297,9 +297,9 @@ await test("setup: from-home key step + settle auto-detects the enrolment return
settle.fn();
await tick();
await tick();
- assert.deepEqual(h.records.find((r) => r.step === "keys-home"), { step: "keys-home", label: "Saved your credentials securely with a passkey", value: "passkey" });
+ assert.deepEqual(h.records.find((r) => r.step === "keys-home"), { step: "keys-home", label: "Signed in — your space's key is safe", value: "passkey" });
assert.deepEqual(h.records.at(-1), { step: "finish", label: "Your space is ready", value: "done" });
- assert.ok(h.transcript.find((t) => /safely backed up behind your passkey. ✅/.test(t.text)));
+ assert.ok(h.transcript.find((t) => /your space's key is safe. ✅/.test(t.text)));
assert.ok(h.verbLog.find((v) => v[0] === "markSetupComplete"), "reached finish without a tap");
});
@@ -333,7 +333,7 @@ await test("setup: native creator — greeting shows, name pre-set, passkey neve
assert.deepEqual(h.records.at(-1), { step: "finish", label: "Your space is ready", value: "done" });
assert.ok(h.verbLog.find((v) => v[0] === "markSetupComplete"));
assert.equal(h.verbLog.some((v) => v[0] === "deferBackupReminders"), false);
- assert.equal(h.transcript.some((t) => /come back to that key backup/.test(t.text)), false);
+ assert.equal(h.transcript.some((t) => /One more reminder/.test(t.text)), false);
const chipLabels = h.transcript.flatMap((t) => t.chips || []); // harness stores labels
assert.equal(chipLabels.includes("Take the tour"), false, "the tour stays web-only");
assert.ok(chipLabels.includes("Open admin"), "native hops to Admin");
@@ -378,7 +378,7 @@ await test("welcome: invited fresh join — passkey probe, suggested name, photo
// Passkey first; the ceremony stays put → probe routes to the waiting step.
// (The step also arms a background settle poll; target the probe by interval.)
- await h.tap("Save my access (recommended)");
+ await h.tap("Sign in (recommended)");
const accessProbe = h.pendingDelays.find((d) => d.ms === 1500);
assert.ok(accessProbe, "the Save-my-access probe schedules a 1500ms check");
accessProbe.fn();
@@ -386,11 +386,11 @@ await test("welcome: invited fresh join — passkey probe, suggested name, photo
assert.match(h.lastText(), /I'll wait here while you finish/);
h.enrolled = true;
await h.tap("Continue");
- assert.deepEqual(h.records[0], { step: "access", label: "Saved your access securely with a passkey", value: "passkey" });
+ assert.deepEqual(h.records[0], { step: "access", label: "Signed in — your access is safe", value: "passkey" });
// The ack folds into the name question; the suggested-name chip is offered.
const nameQ = h.transcript.find((t) => t.extra.key === "q-name");
- assert.match(nameQ.text, /^Perfect — that's saved\. 🔒\n\nFirst things first/);
+ assert.match(nameQ.text, /^Perfect — you're all set\. 🔒\n\nFirst things first/);
await h.tap("I'm Luan");
assert.deepEqual(h.verbLog.find((v) => v[0] === "setMyName"), ["setMyName", "Luan"]);
assert.equal(h.records[1].label, "noted: you're Luan");
@@ -415,7 +415,7 @@ await test("welcome: invited fresh join — passkey probe, suggested name, photo
assert.ok(h.verbLog.find((v) => v[0] === "markWelcomeDone"));
const fin = h.transcript.find((t) => t.extra.key === "q-finish");
assert.match(fin.text, /you're all set\. 🎉 Enjoy Test Space!/);
- assert.equal(/access backup/.test(fin.text), false);
+ assert.equal(/One more reminder/.test(fin.text), false);
await h.tap("Say hi to Ciara");
assert.ok(h.verbLog.find((v) => v[0] === "openPendingThread"));
assert.ok(h.lastChips.find((c) => c.label === "Take the tour"));
@@ -444,8 +444,8 @@ await test("welcome: skip everything — nudge acks, photo skip disarms, nag on
// Finish: skipped-photo comfort line + the backup nag; no pending-thread chip.
const fin = h.transcript.find((t) => t.extra.key === "q-finish");
assert.match(fin.text, /^No problem — you can add one anytime from your profile\./);
- assert.match(fin.text, /come back to that access backup soon/);
- assert.deepEqual(h.lastChips.map((c) => c.label), ["Take the tour"]);
+ assert.match(fin.text, /One more reminder before you go/);
+ assert.deepEqual(h.lastChips.map((c) => c.label), ["Sign in", "Take the tour"], "the parting sign-in chip rides the finish");
});
await test("welcome: native embed — passkey step skipped, access secured by the sign-in, finish records completion", async () => {
@@ -456,7 +456,7 @@ await test("welcome: native embed — passkey step skipped, access secured by th
// The passkey ask never renders; the access step records the native reason
// and the conversation goes straight from the greeting to the name.
- assert.equal(h.transcript.some((t) => /Shall we save it behind a passkey/.test(t.text)), false);
+ assert.equal(h.transcript.some((t) => /Sign in with Eleven and your access is safe/.test(t.text)), false);
assert.deepEqual(h.records[0], { step: "access", label: "Your access is already secured by your Eleven sign-in", value: "native" });
assert.ok(h.transcript.find((t) => t.extra.key === "q-name"));
@@ -469,7 +469,7 @@ await test("welcome: native embed — passkey step skipped, access secured by th
const fin = h.transcript.find((t) => t.extra.key === "q-finish");
assert.ok(fin, "finish reached without a photo ask");
assert.equal(h.transcript.some((t) => t.extra.key === "q-photo"), false);
- assert.equal(/access backup/.test(fin.text), false, "no backup nag — access is the sign-in");
+ assert.equal(/One more reminder/.test(fin.text), false, "no sign-in nag — access is the sign-in");
assert.ok(h.records.find((r) => r.step === "finish" && r.value === "done"), "finish records the completion marker");
});
@@ -484,8 +484,8 @@ await test("welcome: passkey-return leg — richer ack, no generic re-greet, que
// never renders.
assert.equal(h.transcript.some((t) => /^Welcome back — let's pick up/.test(t.text)), false);
assert.ok(h.transcript.find((t) => /^Great — welcome back\. 🔒/.test(t.text)));
- assert.equal(h.transcript.some((t) => /Shall we save it behind a passkey/.test(t.text)), false);
- assert.deepEqual(h.records[0], { step: "access", label: "Saved your access securely with a passkey", value: "passkey" });
+ assert.equal(h.transcript.some((t) => /Sign in with Eleven and your access is safe/.test(t.text)), false);
+ assert.deepEqual(h.records[0], { step: "access", label: "Signed in — your access is safe", value: "passkey" });
assert.ok(h.transcript.find((t) => t.extra.key === "q-name"));
// A later reload (name set, access recorded): generic re-greet, hello skipped.
@@ -508,12 +508,12 @@ await test("welcome: enrolment confirmed while on access-waiting settles through
runFlow(WELCOME_FLOW, h.env, h.io);
await tick();
- await h.tap("Save my access (recommended)"); // fire-and-forget verb; page stays (stay mode)
+ await h.tap("Sign in (recommended)"); // fire-and-forget verb; page stays (stay mode)
const probe = h.pendingDelays.find((d) => d.ms === 1500);
assert.ok(probe, "the save chip arms its fall-to-waiting probe");
probe.fn();
await tick();
- assert.ok(h.transcript.find((t) => /I'll wait here while you finish saving/.test(t.text)), "fell to access-waiting");
+ assert.ok(h.transcript.find((t) => /I'll wait here while you finish signing in/.test(t.text)), "fell to access-waiting");
// The poll confirms; access-waiting's settle re-enters access, whose entry
// records the backup and acks — no Continue tap.
@@ -524,7 +524,7 @@ await test("welcome: enrolment confirmed while on access-waiting settles through
await tick();
await tick();
assert.deepEqual(h.records.find((r) => r.step === "access"),
- { step: "access", label: "Saved your access securely with a passkey", value: "passkey" });
+ { step: "access", label: "Signed in — your access is safe", value: "passkey" });
assert.ok(h.transcript.find((t) => /^Great — welcome back\. 🔒/.test(t.text)));
assert.ok(h.transcript.find((t) => t.extra.key === "q-name"), "carried on to the name step");
});
@@ -551,11 +551,11 @@ await test("setup: enrolment confirmed while on keys-waiting settles through", a
await tick();
await tick();
assert.deepEqual(h.records.find((r) => r.step === "keys"),
- { step: "keys", label: "Saved your credentials securely with a passkey", value: "passkey" });
- assert.ok(h.transcript.find((t) => /safely backed up behind your passkey. ✅/.test(t.text)));
+ { step: "keys", label: "Signed in — your space's key is safe", value: "passkey" });
+ assert.ok(h.transcript.find((t) => /your space's key is safe. ✅/.test(t.text)));
assert.ok(h.verbLog.find((v) => v[0] === "markSetupComplete"), "reached finish without a tap");
// Backed up → the finish bubble must NOT nag about coming back to it.
- assert.equal(h.transcript.some((t) => /come back to that key backup/.test(t.text)), false);
+ assert.equal(h.transcript.some((t) => /One more reminder/.test(t.text)), false);
});
// ---- native bundle drift gate ------------------------------------------------
diff --git a/web/static/apps/setup.flow.js b/web/static/apps/setup.flow.js
index 4aeff2d..cc74523 100644
--- a/web/static/apps/setup.flow.js
+++ b/web/static/apps/setup.flow.js
@@ -206,18 +206,20 @@ export default {
{
if: "bridge.enrolled",
set: { "vars.backedUp": true },
- recordIfNew: { label: "Saved your credentials securely with a passkey", value: "passkey" },
- sayIfNew: "Your key is safely backed up behind your passkey. ✅",
+ recordIfNew: { label: "Signed in — your space's key is safe", value: "passkey" },
+ sayIfNew: "You're signed in — your space's key is safe. ✅",
goto: "finish",
},
// Someone who created this space from their Eleven sign-in page already
- // holds a passkey — don't pitch passkeys from scratch, offer to add
- // this space's key to theirs (keys-home).
+ // holds an account — don't pitch signing in from scratch, offer to add
+ // this space's key to it (keys-home).
{ if: "bridge.fromHome", goto: "keys-home" },
],
- // The passkey ceremony returns asynchronously (a redirect back from the
+ // The sign-in ceremony returns asynchronously (a redirect back from the
// home server); auto-detect that return and advance without a manual tap.
settle: { when: "bridge.enrolled" },
+ // Member-facing language is "sign in" — never the mechanism (passkeys,
+ // backups). The lock-and-key framing carries the stakes instead.
bubbles: [
{
text:
@@ -225,7 +227,7 @@ export default {
"Everything here is locked with a key that exists only on your devices. There's no password reset and no support desk — not even whoever runs the server can read your messages or recover that key. That's not a flaw; it's the whole point.\n\n" +
"But it means: right now, this browser holds the only key to your space. If it's lost — cleared, broken, stolen — your space is gone with it, and nobody can get it back.",
},
- { text: "Sign in with Eleven and your key stays safe with your passkey — one tap, and you'll be brought straight back." },
+ { text: "Sign in with Eleven and your key stays safe — one tap, your face or fingerprint, and you'll be brought straight back." },
],
chips: [
{
@@ -240,19 +242,19 @@ export default {
{
// The from-home variant of "keys": they signed in with Eleven to create
// this space, so the ask is one addition, not a new concept. Mechanics
- // are identical (the enrol popup signs into their existing passkey and
+ // are identical (the enrol popup signs into their existing account and
// merges this space's key + name over the address-only stub).
id: "keys-home",
entries: [
{
if: "bridge.enrolled",
set: { "vars.backedUp": true },
- recordIfNew: { label: "Saved your credentials securely with a passkey", value: "passkey" },
- sayIfNew: "Your key is safely backed up behind your passkey. ✅",
+ recordIfNew: { label: "Signed in — your space's key is safe", value: "passkey" },
+ sayIfNew: "You're signed in — your space's key is safe. ✅",
goto: "finish",
},
],
- // Auto-detect the return from the home server's passkey ceremony (same as
+ // Auto-detect the return from the home server's sign-in ceremony (same as
// the keys step above) so the user is carried on without tapping Continue.
settle: { when: "bridge.enrolled" },
bubbles: [
@@ -261,7 +263,7 @@ export default {
"Last thing — the key. 🔑\n\n" +
"This space is locked with a key that was just created here, in this browser. Your Eleven sign-in doesn't hold it yet — so if this browser is lost, the space goes with it.",
},
- { text: "Sign in and it's added to your passkey — one tap, and you'll be brought straight back." },
+ { text: "Sign in and it's added to your account — one tap, and you'll be brought straight back." },
],
chips: [
{
@@ -286,8 +288,8 @@ export default {
step: "keys",
label: {
if: "bridge.enrolled",
- then: "Saved your credentials securely with a passkey",
- else: "Saved your credentials with a passkey (in your browser)",
+ then: "Signed in — your space's key is safe",
+ else: "Signed in (in this browser)",
},
value: "passkey",
},
@@ -297,7 +299,7 @@ export default {
{
label: "It didn't work — skip for now",
spent: "Skipped",
- record: { step: "keys", label: "Skipped the credentials backup for now", value: "skipped" },
+ record: { step: "keys", label: "Skipped signing in for now", value: "skipped" },
set: { "vars.backedUp": false },
goto: "finish",
},
@@ -310,12 +312,12 @@ export default {
settle: { when: "bridge.enrolled", goto: "keys" },
bubbles: [
{
- text: "I have to be straight with you: without a backup, one lost or cleared browser means this space and everything in it is gone — permanently, for everyone, and no one can help. I'll remind you again in a day or so.",
+ text: "I have to be straight with you: if you don't sign in, one lost or cleared browser means this space and everything in it is gone — permanently, for everyone, and no one can help. I'll remind you again in a day or so.",
},
],
chips: [
{
- label: "Go back and save it",
+ label: "Go back and sign in",
advancing: false,
effect: { verb: "savePasskey" },
probe: { afterMs: 1500, unless: "bridge.enrolled", goto: "keys-waiting" },
@@ -323,7 +325,7 @@ export default {
{
label: "I understand the risk",
spent: "Understood",
- record: { step: "keys", label: "Skipped the credentials backup for now", value: "skipped" },
+ record: { step: "keys", label: "Skipped signing in for now", value: "skipped" },
set: { "vars.backedUp": false },
goto: "finish",
},
@@ -342,12 +344,21 @@ export default {
text: [
"That's it — {space.name|your space} is ready! 🎉\n\n",
"Everything we just set up lives in your Admin panel — invite links, the space's name and photo, who can invite. Type `/admin` anywhere to open it; it's yours alone as this space's admin.",
- { if: "!vars.backedUp", text: "\n\nAnd do come back to that key backup soon." },
+ // The one honest parting warning for an admin who chose "Not now":
+ // plain consequences, no mechanism talk, and the way out right there.
+ {
+ if: "!vars.backedUp",
+ text:
+ "\n\nOne more reminder before you go: you haven't signed in, so this browser holds the only key to this space. If it's lost or cleared, the space is gone with it — for everyone. Sign in any time and it's safe.",
+ },
],
},
{ text: "Where would you like to start?" },
],
chips: [
+ // The parting reminder's action — finish-line chips stay live, and the
+ // completion poll picks the sign-in up even after the flow has ended.
+ { if: "!vars.backedUp", label: "Sign in", advancing: false, effect: { verb: "savePasskey" } },
// Native hops threads too (the conversation model's openAdmin verb).
{ label: "Open admin", advancing: false, effect: { verb: "openAdmin" } },
// The tour is the web's guided overlay — web-only for now.
diff --git a/web/static/apps/welcome.flow.js b/web/static/apps/welcome.flow.js
index 18774d0..88ea08d 100644
--- a/web/static/apps/welcome.flow.js
+++ b/web/static/apps/welcome.flow.js
@@ -47,30 +47,31 @@ export default {
},
{
if: { any: ["bridge.enrolled", "done"] },
- recordIfNew: { label: "Saved your access securely with a passkey", value: "passkey" },
- // Just back from the passkey ceremony: acknowledge it worked, then
+ recordIfNew: { label: "Signed in — your access is safe", value: "passkey" },
+ // Just back from the sign-in ceremony: acknowledge it worked, then
// move on — a separate bubble, so the ack lands before the question.
- sayIfNew: "Great — welcome back. 🔒 Now that your access is secure, let's continue.",
+ sayIfNew: "Great — welcome back. 🔒 You're signed in, so your access is safe. Let's continue.",
goto: "name",
},
],
- // The passkey ceremony returns asynchronously (a redirect back, or tabbing
+ // The sign-in ceremony returns asynchronously (a redirect back, or tabbing
// back from a system browser); auto-detect that return and carry on without
// a manual "Continue" tap.
settle: { when: "bridge.enrolled" },
// One bubble, chips attached — context and ask together, so the opening
- // run is two messages (intro + this), not three.
+ // run is two messages (intro + this), not three. The member-facing word
+ // is "sign in" — never the mechanism (passkeys, backups).
bubbles: [
{
text:
"Before anything else, let's make sure you can't lose your way in. 🔑\n\n" +
"Your access to this space lives in a key that exists only in this browser — there's no username or password, and nobody can reset it for you. If this browser's data is cleared, your access goes with it.\n\n" +
- "Shall we save it behind a passkey now? One tap — you'll be brought straight back and we'll carry on.",
+ "Sign in with Eleven and your access is safe — one tap, your face or fingerprint, and you'll be brought straight back so we can carry on.",
},
],
chips: [
{
- label: "Save my access (recommended)",
+ label: "Sign in (recommended)",
advancing: false,
effect: { verb: "savePasskey" },
probe: { afterMs: 1500, unless: "bridge.enrolled", goto: "access-waiting" },
@@ -79,7 +80,7 @@ export default {
label: "Not now",
spent: "Skipped",
effect: { verb: "deferBackupReminders" },
- record: { label: "Skipped the access backup for now", value: "skipped" },
+ record: { label: "Skipped signing in for now", value: "skipped" },
ack: "No problem — I'll nudge you again in a day or so.",
goto: "name",
},
@@ -90,7 +91,7 @@ export default {
// The ceremony may confirm while we sit here (the completion poll flips
// the flag) — re-enter access so its entry records + acks, no tap needed.
settle: { when: "bridge.enrolled", goto: "access" },
- bubbles: [{ text: "I'll wait here while you finish saving the passkey. When you're done, tap Continue." }],
+ bubbles: [{ text: "I'll wait here while you finish signing in. When you're done, tap Continue." }],
chips: [
{
label: "Continue",
@@ -98,19 +99,19 @@ export default {
step: "access",
label: {
if: "bridge.enrolled",
- then: "Saved your access securely with a passkey",
- else: "Saved your access with a passkey (in your browser)",
+ then: "Signed in — your access is safe",
+ else: "Signed in (in this browser)",
},
value: "passkey",
},
- ack: "Perfect — that's saved. 🔒",
+ ack: "Perfect — you're all set. 🔒",
goto: "name",
},
{
label: "It didn't work — skip for now",
spent: "Skipped",
effect: { verb: "deferBackupReminders" },
- record: { step: "access", label: "Skipped the access backup for now", value: "skipped" },
+ record: { step: "access", label: "Skipped signing in for now", value: "skipped" },
ack: "No problem — I'll nudge you again in a day or so.",
goto: "name",
},
@@ -235,13 +236,22 @@ export default {
text: [
{ if: "vars.skippedPhoto", text: "No problem — you can add one anytime from your profile.\n\n" },
"That's it — you're all set. 🎉 Enjoy {space.name|this space}!",
- { if: "!accessBackedUp", text: "\n\nAnd do come back to that access backup soon." },
+ // The one honest parting warning for someone who chose "Not now":
+ // plain consequences, no mechanism talk, and the way out right there.
+ {
+ if: "!accessBackedUp",
+ text:
+ "\n\nOne more reminder before you go: you haven't signed in yet, so your access to this space lives only in this browser — if it's cleared or lost, there's no way back in. Sign in any time and you're safe.",
+ },
],
key: "q-finish",
},
{ text: "Where would you like to start?" },
],
chips: [
+ // The parting reminder's action — finish-line chips stay live, and the
+ // completion poll picks the sign-in up even after the flow has ended.
+ { if: "!accessBackedUp", label: "Sign in", advancing: false, effect: { verb: "savePasskey" } },
{
if: { all: ["hasPendingThread", "inviterName"] },
label: "Say hi to {inviterName}",