Files
Eduard Gert d4451274c7 Replace the pending-approval error page with a dedicated screen (#802)
* Replace the pending-approval error page with a dedicated screen

A user waiting for an administrator to approve their account was shown
the generic /error page: an error icon, a raw response_message dump and
"if you believe this is an error, please contact your administrator".
Waiting for approval is an expected part of signing up, so it now gets a
screen of its own, following the onboarding layout of logo, centered card
and a stepper placing the user between "Account Created" and
"Join Account".

The owner to contact is read out of the refusal management sends, which
names a masked address, and the copy asks for the owner without naming
one when the address is absent.

Steps.Step gains an optional status and size, so a step can render as
complete, current or upcoming at a larger scale. Steps that pass neither
keep the look used by the instruction-list steppers.

* Detect the pending-approval screen by test id in e2e

The three places that recognise a pending user matched on "User Approval
Pending", the heading the old error page rendered. The screen that
replaced it has no heading, so the approval branch of the login races
could never resolve and every leg timed out instead: both fixtures treat
approval as a legitimate outcome, not a failure.

They now key off the data-testid on the screen's root, which does not
move when the copy does.

* Point the pending-approval footer at the docs and support pages

A mailbox asks the reader to compose something; the approval docs explain
what they are waiting for, and the support page routes them the way every
other NetBird surface does. The support address constant went with it.

* Render the pending-approval screen from the caller-identity response

Every call a pending user makes is refused, but only /users/current
names the owner who can approve them. Redirecting to /error on whichever
403 landed first therefore threw that name away about as often as it
kept it, and which screen appeared came down to a race.

Pending users are no longer routed to /error at all. The provider that
already waits on /users/current renders the screen from that response,
which also ends the wait it could never finish: the caller identity it
blocks on is exactly what such a user is refused.

* Decide the refused-user screen from the responses, not the first 403

Routing on whichever refused call landed first meant the screen depended
on a race, and once only /users/current could name the owner it meant
the name was usually thrown away. Management older than that reports a
pending user as merely blocked there, so it also meant a pending user
landed on the blocked screen, which is what the e2e run caught.

UserProfileProvider now decides from the responses it already waits on:
/users/current first, since it alone names the owner, then the user list,
which reports pending on every management version. Blocked keeps the
error page, decided after pending so the older wording cannot win.

* Wait for both refusal responses before choosing a screen

Concluding blocked the moment /users/current said so meant concluding it
while the user list was still in flight — and on management that reports
a pending user as merely blocked there, that list is the one response
that would have said otherwise. The screen it was meant to prevent was
reachable again, just through a narrower window.

The decision moves into resolveRefusedUser, which holds off while a
better-informed response is still coming: the named one before the list,
the list before blocked. Navigating to the error page becomes an effect
rather than a call made while rendering.

* Move the refused-user decision next to the provider that makes it

It sat in utils only because two callers needed it, but the second one —
the guard keeping these errors off the error boundary — needs nothing
more than to recognise them, which is a line of its own. So the decision
moves beside the provider that acts on it, and reads an error into a
RefusalReason rather than assembling a discriminated union.

The cost is that both files now spell out management's wording. The one
here is the tested reading; the one in @utils/api only has to be no
narrower than it.

* Assert the refusal kind the resolver actually returns

The result property is kind, not reason. toEqual types its argument
loosely, so the stale expectations type-checked and only failed when run.
2026-09-21 10:33:43 +02:00
..
2026-08-27 10:08:22 +02:00