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>
7 files changed,
+141
−85
changelog/2026-07-11-sign-in-words.md+1 −0ios/LChatCore/Sources/LChatCore/Flows/setup.json+35 −13ios/LChatCore/Sources/LChatCore/Flows/welcome.json+23 −11ios/LChatCore/Tests/LChatCoreTests/FlowEngineTests.swift+3 −3test/flow.test.mjs+26 −26web/static/apps/setup.flow.js+29 −18web/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}", |