Skip to content

feat(register): Phase 3a — UIAA dummy flow for self-hosted servers#115

Open
TigerInYourDream wants to merge 23 commits intofix/registerfrom
feat/register-phase-3a-dummy-uiaa
Open

feat(register): Phase 3a — UIAA dummy flow for self-hosted servers#115
TigerInYourDream wants to merge 23 commits intofix/registerfrom
feat/register-phase-3a-dummy-uiaa

Conversation

@TigerInYourDream
Copy link
Copy Markdown

Summary

Phase 3a eliminates the "Phase 3 will handle the form" placeholder on UIAA servers. Adds an inline registration form + single-stage m.login.dummy handler so a robrix2 user can create a Matrix account on a UIAA server (e.g. local Palpo at http://192.168.1.58:8128) end-to-end and be auto-logged in, without leaving the app.

Stacked on #114 (fix/register). When #114 lands, this PR auto-rebases onto main.

What landed

  • Inline RegistrationForm (username, password + confirm, submit) shown only when mode() == Uiaa; hidden in MAS / Disabled paths
  • UIAA two-step dance handler (MatrixRequest::RegisterViaUiaa) that repackages the orphan LoginRequest::Register branch at src/sliding_sync.rs:435-501 — first POST gets 401+session, second POST sends AuthData::Dummy(session), 200 returns access_token
  • Screen transition on success via dual dispatch: RegisterAction::RegistrationSuccess (early bridging feedback) then LoginAction::LoginSuccess (app.rs swaps to main UI after sync service builds)
  • UiaaStage trait + DummyStage as the extensibility seam for Terms / RegistrationToken stages in future phases
  • Validators: validate_localpart (lowercase + ._=-/), validate_passwords_match (empty + equality) with 7 unit tests
  • Enter-key submit from any input (mirrors LoginScreen)

Correctness fixes discovered during review (Codex rounds 1 & 2)

  • Out-of-order discovery responses dropped — echo requested_url with CapabilitiesDiscovered / DiscoveryFailed, compare against last_discovery_input_url
  • Stale cache invalidated on new Next click — clear last_discovery + hide form before firing a fresh probe
  • Back blocked during submitregistration_pending gate so users don't get auto-logged-in after navigating away
  • Stale-cache check uses user input, not caps.base_url.well-known rewrites URLs, so strict string compare on base_url false-triggers on every well-known-delegated server
  • Post-register sync-startup failure recoveryawaiting_sync_startup flag gates a LoginFailure arm on RegisterScreen; app.rs can't recover because state is still LoggedOut
  • Modal coherencesuppress_login_failure_modal on LoginScreen + ClearFailureState action prevent LoginScreen's modal from stacking on top of RegisterScreen's recovery text
  • Full state reset on success — passwords and all form fields cleared so the same widget instance is clean on re-entry after logout
  • form_error_label visibility — was calling .view() on a Label (silently no-op), switched to .label() accessor

UX polish

  • "Creating..." on submit button while register POST is in flight
  • "Checking..." on next_button during slow .well-known probes (matrix.org can take 3-5s)
  • Pending gates: duplicate Next during submit / post-register sync window are swallowed
  • Recovery message on sync-startup failure — "Your account was created, but we couldn't start a session... Please click ← Back to Login and sign in with your new account." (account exists on server; don't frame as registration failure)

Architecture notes

  • Lean reuse: no new matrix-sdk code, no new deps in Cargo.toml. Reuses registration_request() helper, finalize_authenticated_client(), and the login_sender channel
  • 5 state fields on RegisterScreen, each mapped to a specific UX / race-condition concern (no speculative state)
  • Box<HsCapabilities> shrinks the RegisterAction enum so tiny variants don't pay for UiaaInfo + SSO providers
  • Worker-loop shutdown no longer logs a spurious error during graceful logout / account switch

Scope (explicitly deferred)

Phase 3a is MAS + m.login.dummy-only — Synapse default requires m.login.terms, matrix.org requires reCAPTCHA, closed Synapses require m.login.registration_token. See "Intentionally Deferred" section of plan.

Stage Phase
m.login.terms deferred (no accessible test server)
m.login.registration_token deferred
Email / msisdn 3pid Phase 4
SSO buttons from /login Phase 4
Username availability debounce (/register/available) Phase 5
zxcvbn password strength Phase 5
OAuth callback for auto-login-after-MAS-signup future phase

Not element-desktop parity

This PR brings us to ~55-80% of real-world Matrix servers (Palpo/Dendrite/Conduit default + MAS-delegated). It is not feature-equivalent to element-web's Registration.tsx — see scope table above.

Test plan

  • cargo test --lib register — 21 passing (validators + uiaa + register_screen pure-fn tests)
  • cargo test --lib login::login_screen — 3 passing (modal suppression pure-fn tests)
  • cargo test --lib sliding_sync — 7 passing (includes worker_shutdown_is_unexpected)
  • cargo clippy --lib --no-deps -- -W clippy::all — 0 new warnings on touched files
  • Manual: successful UIAA registration against local Palpo → auto-login to main UI
  • Manual: M_USER_IN_USE error renders in form_error_label after visibility fix
  • Manual: regression — MAS path (alvin.meldry.com) still opens browser
  • Manual: regression — Disabled path shows the right message
  • Manual: Back during submit swallowed; re-entry shows clean screen
  • Manual: slow probe (matrix.org) shows "Checking..." on next_button

🤖 Generated with Claude Code

Align RegisterScreen's outer shell with origin/main LoginScreen structure:
set_type_default + SolidView base, Overlay root with centered alignment,
ScrollYView with hidden scrollbar, RoundedView card wrapper with 50/50
margin, inner column with spacing 15.0. Logo references the shared
mod.widgets.IMG_APP_LOGO token (registered by login_screen.rs script_mod
which runs first in app.rs:1549).
Retroactively commit the planning documents that guided the Phase 1+2
implementation already merged into this branch. These were written during
PR#114 development but never checked in.
Add RegistrationSubmitted / RegistrationSuccess / RegistrationFailed(String)
variants used by the upcoming UIAA register flow. No behavior change yet —
these are dispatched by the sliding_sync handler (Task 5) and consumed by
RegisterScreen (Task 6) and app.rs (Task 7).
validate_localpart() enforces Matrix historical localpart grammar
(lowercase alnum + ._=-/), length 1-255. validate_passwords_match() checks
both fields non-empty and equal. Seven unit tests cover empty / mismatch /
long / uppercase / happy paths. Used by Task 6 RegistrationForm submit
validation.
UiaaStage is the extensibility seam for future stages (Terms, Token,
Email, etc.). Each stage decides via auto_submit() whether it can advance
without user input (dummy case — returns Some(AuthData)) or needs UI
(returns None — future stages). DummyStage covers the Palpo / default
Synapse case. Two unit tests verify dummy auto-submit and stage type
string compliance with Matrix spec.
…aps.base_url

Previously the stale-cache guard compared the current normalized input
against caps.base_url, which .well-known/matrix/client may rewrite
(trailing slash, federation host). This false-triggered the 'homeserver
changed' branch on every well-known-delegated server, silently hiding
the registration form and the error label inside it — making it look
like the app had navigated away.

Fix by tracking the user-intent URL (last_discovery_input_url) that
produced the discovery and comparing against that. Also stop hiding
the form on mismatch — only clear the cache and show the error, since
form_error_label lives inside the form and must stay visible.

Removes diagnostic logging added in the previous commit.
Drop out-of-order discovery responses by echoing requested_url back with
CapabilitiesDiscovered / DiscoveryFailed. RegisterScreen compares the
response's requested_url against last_discovery_input_url (which tracks
what the user most recently asked about) and skips stale actions.

Without this, rapid Next-A → Next-B where A returns late would overwrite
B's capabilities in the UI and route a subsequent register request to A.
When the user clicks Next for a different homeserver, clear the old
last_discovery cache and hide the registration_form before the new
probe returns. Prevents a submit click landing between Next and the
response from firing a register request against the previous server.

Combined with the requested_url echo from the previous commit, the
race window is closed on both sides: the late response is dropped,
and the stale cache it would have resolved to is gone.
The register request is already in login_sender's queue by the time
the user might reach for Back, and we don't have a cancellation path.
If the POST succeeds after navigation, LoginSuccess auto-logs the user
into an account they thought they were walking away from.

Swallow Back clicks during registration_pending; the terminal Success
or Failed action will clear the flag, at which point Back works again.
Clear all form inputs + homeserver_input + last_discovery state the
moment RegistrationSuccess fires. Passwords are cleared first so no
credential text lingers in widget memory. The registration_form is
hidden, but status_label keeps 'Account created! Loading...' as
bridging feedback during the ~100-200ms until LoginSuccess lands.

On LoginAction::LoginSuccess (dispatched after the sync service
finishes building), hide status_area and clear status_label so the
next entry into this screen is a completely fresh state — required
because the same RegisterScreen widget instance is reused when the
user logs out and taps 'Sign up here' again.
…ccess

Between RegisterAction::RegistrationSuccess and LoginAction::LoginSuccess
the sync service is still being built. If SyncService::build() fails
there (network blip, token validation race), the login loop emits
LoginAction::LoginFailure — but app.rs only acts on that failure when
the app is NOT in LoggedOut state, and we haven't reached LoginSuccess
yet. Previously RegisterScreen only handled LoginSuccess, so the screen
stayed stuck on 'Account created! Loading your account...' forever.

Add an awaiting_sync_startup flag set on RegistrationSuccess and
cleared on either terminal action. When gated by the flag, a LoginFailure
replaces the loading text with a recovery message pointing the user at
the login screen — the account does exist on the server, so framing this
as a registration failure would be wrong.
- Shrink RegisterAction: box HsCapabilities so enum size tracks the small
  variants instead of UiaaInfo + providers + strings.
- Gate Next click on registration_pending / awaiting_sync_startup via a
  pure helper so rapid clicks during submit or post-register sync startup
  don't fire a new probe.
- Modal coherence: suppress LoginScreen's failure modal while the register
  flow is active, with a ClearFailureState action so the modal closes when
  RegisterScreen itself catches the LoginFailure.
- Render form_error_label on registration failures (was calling .view()
  on a Label, silently no-op on set_visible); also clear the text on
  clear_form_error so stale messages don't linger.
- Show 'Checking...' on next_button during slow .well-known probes so the
  click is visibly acknowledged even on servers where discovery takes 3-5s.
- Graceful matrix_worker_task shutdown suppresses the spurious 'ended
  unexpectedly' log during logout / account switch.
- Remove redundant M_FORBIDDEN arm in discover_homeserver_capabilities
  (the else branch produces the same tuple).

Adds pure-function tests for can_start_capability_discovery,
should_show_login_failure_modal, and worker_shutdown_is_unexpected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant