From c29cbcd549f72840565fa1b1c3e7dad877765c8d Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 1 Aug 2026 13:28:31 +0200 Subject: [PATCH] feat: step-by-step sign-in with server discovery from the email address --- mockups/login.html | 953 +++++++++++++++++++++ src/components/QrScanModal.tsx | 16 +- src/lib/__tests__/login-errors.test.ts | 59 ++ src/lib/__tests__/server-discovery.test.ts | 184 ++++ src/lib/login-errors.ts | 102 +++ src/lib/server-discovery.ts | 170 ++++ src/screens/LoginScreen.tsx | 608 +++++++------ src/screens/login/ChooseStep.tsx | 184 ++++ src/screens/login/ConfirmStep.tsx | 119 +++ src/screens/login/EmailStep.tsx | 87 ++ src/screens/login/LoginNotice.tsx | 52 ++ src/screens/login/LoginShell.tsx | 107 +++ src/screens/login/OptionTile.tsx | 91 ++ src/screens/login/PasswordStep.tsx | 98 +++ src/screens/login/ServerStep.tsx | 88 ++ src/screens/login/SigningInStep.tsx | 102 +++ 16 files changed, 2744 insertions(+), 276 deletions(-) create mode 100644 mockups/login.html create mode 100644 src/lib/__tests__/login-errors.test.ts create mode 100644 src/lib/__tests__/server-discovery.test.ts create mode 100644 src/lib/login-errors.ts create mode 100644 src/lib/server-discovery.ts create mode 100644 src/screens/login/ChooseStep.tsx create mode 100644 src/screens/login/ConfirmStep.tsx create mode 100644 src/screens/login/EmailStep.tsx create mode 100644 src/screens/login/LoginNotice.tsx create mode 100644 src/screens/login/LoginShell.tsx create mode 100644 src/screens/login/OptionTile.tsx create mode 100644 src/screens/login/PasswordStep.tsx create mode 100644 src/screens/login/ServerStep.tsx create mode 100644 src/screens/login/SigningInStep.tsx diff --git a/mockups/login.html b/mockups/login.html new file mode 100644 index 00000000..62d00855 --- /dev/null +++ b/mockups/login.html @@ -0,0 +1,953 @@ +
Bulwark Mobile · sign-in redesign
++ The current sign-in screen opens with a Server URL field. Most people don't know their + JMAP endpoint, and the two paths that don't require knowing it — the webmail handoff and the QR code — sit + below a divider, greyed out until the URL is filled in anyway. This proposes an entry screen that leads with + the easy paths and asks for a server only when we genuinely can't work it out. +
+ + + ++ Everything at once, in the least helpful order. The webmail handoff is the safer route — it never puts a + password into the app — but it's disabled until you've supplied the server, so it can't rescue anyone who's stuck. +
+Secure. Private. Yours.
++ The two disabled controls at the bottom are the ones that would have helped. Nothing tells you where + to find a server URL, and nothing happens if you don't have one. +
++ One question per screen, cheapest question first. The screens below are the happy path, left to right. +
+Secure. Private. Yours.
++ Two live options instead of six mixed ones. Each tile says what it costs you before you tap it. + Manual setup stays reachable in one tap — it just isn't the greeting. +
+That's usually all we need — we'll look up the mail server for you.
+ +Works with Bulwark, Stalwart, and any JMAP mail server.
++ Autofocus with the email keyboard, autocorrect off. The button stays enabled and validates on tap, + so nobody is left staring at a dead grey button wondering which field is wrong. +
+Next you'll sign in on your provider's own page.
+ ++ A browser jump with no warning feels like a bug. Naming the server first — and saying why the + password isn't asked for here — turns the detour into a security feature. +
+
+ This is the existing runWebmailHandoff flow, unchanged. It already supports whatever
+ the server offers — password, OAuth, passkeys, SSO — which the in-app form never will.
+
ada@example.com
++ The first sync is the slowest moment in the app. Naming what's arriving makes the wait legible, and + the inbox opens as soon as mailboxes land — the rest fills in behind it. +
++ Self-hosted servers, typo'd passwords, and second accounts are the normal case for this audience, not the + exception. These four screens carry most of the redesign's weight. +
+Open Bulwark on your computer
+Settings → Devices → Add phone shows a code. Point the camera at it.
+Type the code instead
+
+ A scanner with no instructions is a dead end for anyone who hasn't seen the webmail's Devices page.
+ Redeeming a paired code skips the browser entirely — see redeemPairingCode.
+
We couldn't find a server for example.org. If you self-host, the address is the one you use for webmail.
+ +Same address you type in the browser. We'll add https://.
+
+ Not the JMAP session URL — the webmail address they type every day. The scheme is added for them, and
+ a trailing slash or full /jmap path is accepted and trimmed.
+
This server doesn't support the browser sign-in, so we'll ask here.
+ ++ The store's raw error string is replaced with mapped copy per failure: wrong credentials, unreachable + host, bad certificate, 2FA-needs-an-app-password. Each says what to do next. +
+You're signed in to these already.
+ +
+ isAddMode currently shows the same blank three-field form. Most second accounts live on
+ the same server as the first, so offer that first and skip discovery entirely.
+
+ From ada@example.com: try https://example.com/.well-known/jmap, then
+ https://mail.example.com/.well-known/jmap, then the same host the last account used.
+ First 200 wins. Budget the whole thing at two seconds and fall through to Path B on timeout — a
+ slow lookup must never be worse than the manual form.
+
+ Every screen drives an existing store action — loginViaWebmail,
+ loginViaPairing, login. The redesign is routing and copy; the auth
+ surface, the state machine, and parseQrLoginPayload stay as they are.
+
+ A multi-step flow earns a hardware-back handler on Android. Every step goes back one, and step 1 + leaves the flow — never a trapped screen with a filled-in field you can't clear. +
+
+ autoComplete="email" and textContentType="username" on the address field,
+ "password" on the fallback field, so the platform password manager offers to fill and
+ save. The current screen sets neither.
+
Mockup · repos/react-native/mockups/login.html · static HTML, no interaction wired up
+