Files
RenewCMS/features/pending_screen.feature

154 lines
10 KiB
Gherkin

Feature: Pending registration screen
As a logged-in user who has not yet verified their account
I want /register/pending to tell me where the verification link went, let me log out or cancel
my registration, and route me to the right destination no matter which of those calls
succeeds or fails
So that I always know what to do next while waiting for the emailed link, and a device that
verified elsewhere or a server error never strands me on the wrong screen
Background:
Given /register/pending is a "session" route (meta.auth "session"), rendered as PendingPage
inside AuthShell, per docs/design/routes.md; "session" only requires a logged-in account, so
both a verified and an unverified account can land on it without being redirected away by
the router guard
And PendingPage only calls DELETE /v1/auth/register and POST /v1/auth/logout
And RegisterView.vue, on a 200 from POST /v1/auth/register, writes the submitted email to
sessionStorage under the key "renewcms:pendingEmail" before navigating to /register/pending;
this is the only change this slice makes to RegisterView.vue
And on mount, PendingPage reads "renewcms:pendingEmail" from sessionStorage once: if present,
it renders the Default state with that email interpolated into the ResultPanel text; if
absent (for example, the user arrived by logging in as an unverified account rather than by
just registering), it renders the Email unknown state with the generic text
And in both cases the ResultPanel has tone "success", icon "i-lucide-mail-check", title "Check
your email", and two secondary actions, "Log out" and "Cancel registration" (color
"neutral", variant "outline"); the note line ("Opened it on another device? That works too;
you'll log in there first. Can't find it? Check your spam folder. If the link has expired,
cancel your registration and start again.") is rendered through ResultPanel's default slot
as a second paragraph below the description, per components.md's "Slot default for extras"
And "Log out" calls POST /v1/auth/logout, which per loginHandler.Logout always responds 200 and
clears the jwt cookie; on its response, PendingPage calls session.clear() and navigates to
/login, disabling both actions while the call is in flight
And "Cancel registration" opens ConfirmDialog (built by this slice; components.md documents it
but no file exists yet) via useOverlay().create(ConfirmDialog).open(), with title "Cancel
your registration?", description "Your account will be deleted. You can register again with
the same or a different email.", confirmLabel "Cancel registration", cancelLabel "Keep my
account" (components.md's "Other uses" line pairs title/description/confirmLabel/cancelLabel
in that order, so ConfirmDialog exposes a cancelLabel prop alongside confirmLabel even though
the compact props line only names confirmLabel), pendingLabel "Cancelling…", and tone
"danger"; its action prop performs the DELETE /v1/auth/register call
And while the dialog's action is in flight (Cancelling), both buttons are disabled, the confirm
button reads "Cancelling…", aria-busy is set, and the dialog cannot be dismissed, per
ConfirmDialog's general contract in components.md
And PendingPage's DELETE /v1/auth/register call passes { onForbidden: 'passthrough' } (a new
RequestOptions field on useApi, added by this slice, mirroring the existing onUnauthorized
field: type 'redirect' | 'passthrough', default 'redirect'; when 'passthrough' is given, the
403 branch in useApi's request() skips the "if session.session?.verified === false, push to
/register/pending" step entirely and always throws ApiError(403)) so PendingPage's own 403
handling below runs instead of the global handler bouncing it back to /register/pending
And on a 200 from DELETE /v1/auth/register, the action resolves, the dialog closes, the app
navigates to /register, and a toast (color "success", icon "i-lucide-circle-check", title
"Registration cancelled", description "You can create the owner account again.") is shown
And on a 403 from DELETE /v1/auth/register (user.ErrAlreadyVerified, since the account was
verified on another device while this tab still holds a stale unverified session), the
action first awaits session.retry() so the session store's cached verified flag is refreshed
to true — without this, the router guard's stale local state would immediately bounce a
navigation to /home straight back to /register/pending — then the dialog closes,
PendingPage replaces its panel with a ResultPanel (tone "success", icon
"i-lucide-shield-check", title "You're verified", text "Taking you to your dashboard…", no
actions) and shows a toast (color "info", icon "i-lucide-info", title "Your account is already
verified", description "There's nothing to cancel.") over it, exactly as the design board
draws this state, then navigates to /home after the same 800 ms Verify's Success uses
And on a 500 response or a request that never reaches the server, the action rejects, the
dialog stays open, and it shows the failure inline (per ConfirmDialog's general contract:
"the action runs inside the dialog, so errors show there instead of a toast"), re-enabling
both buttons so the user can retry or choose "Keep my account"
And a 401 from either call is handled entirely by useApi's existing global 401 handling
(redirect to /login?redirect=...&reason=expired); PendingPage adds no special case for it
Scenario: Default — the email is known from a just-completed registration
Given sessionStorage holds "renewcms:pendingEmail" set to "marta@renew.example"
When the user navigates to /register/pending
Then a ResultPanel is shown with tone "success" and icon "i-lucide-mail-check"
And its title is "Check your email"
And its text is "We sent a verification link to marta@renew.example. It expires in 2 hours."
And its note reads "Opened it on another device? That works too; you'll log in there first.
Can't find it? Check your spam folder. If the link has expired, cancel your registration and
start again."
And its actions are "Log out" and "Cancel registration", both secondary
Scenario: Email unknown — arrived by logging in as an unverified account
Given sessionStorage holds no "renewcms:pendingEmail" entry
When the user navigates to /register/pending
Then a ResultPanel is shown with tone "success" and icon "i-lucide-mail-check"
And its title is "Check your email"
And its text is "We sent a verification link to the email you registered with. It expires in 2
hours."
And its note is the same as the Default state's note
And its actions are "Log out" and "Cancel registration", both secondary
Scenario: Logging out
Given the user is on /register/pending
When the user activates "Log out"
Then both actions are disabled while POST /v1/auth/logout is in flight
When the response resolves 200
Then the session store is cleared
And the app navigates to /login
Scenario: Confirm cancel — the destructive dialog opens
Given the user is on /register/pending
When the user activates "Cancel registration"
Then a ConfirmDialog opens with title "Cancel your registration?", description "Your account
will be deleted. You can register again with the same or a different email.", a "Keep my
account" cancel button, a "Cancel registration" confirm button in tone "danger", role
"alertdialog", and focus starting on "Keep my account"
And the underlying ResultPanel is still shown behind it, unchanged
Scenario: Cancelling — the DELETE is in flight and the dialog can't be dismissed
Given the ConfirmDialog from "Confirm cancel" is open
When the user activates "Cancel registration" inside the dialog
Then both dialog buttons are disabled, the confirm button reads "Cancelling…", aria-busy is set
on the dialog, and it cannot be dismissed by Escape or an overlay click
And DELETE /v1/auth/register is sent with { onForbidden: 'passthrough' }
Scenario: Keep my account — the dialog is dismissed without deleting anything
Given the ConfirmDialog from "Confirm cancel" is open
When the user activates "Keep my account"
Then the dialog closes, emitting close(false)
And DELETE /v1/auth/register is never called
And the ResultPanel from before the dialog opened is still shown, unchanged
Scenario: Cancelled — 200 navigates to /register with a toast
Given the ConfirmDialog's action is in flight after the user confirmed
When DELETE /v1/auth/register resolves 200
Then the dialog closes
And the app navigates to /register
And a toast is shown with color "success", icon "i-lucide-circle-check", title "Registration
cancelled", description "You can create the owner account again."
Scenario: Already verified — 403 shows the verified panel and toast, then navigates to /home
Given the ConfirmDialog's action is in flight after the user confirmed
And the session store currently holds { verified: false }
When DELETE /v1/auth/register resolves 403 with the apperr.AlreadyVerifiedErrorMessage body
Then session.retry() is awaited before any navigation happens
And once it resolves the session store holds { verified: true }
And the dialog closes
And a ResultPanel is shown with tone "success", icon "i-lucide-shield-check", title "You're
verified", text "Taking you to your dashboard…", and no actions
And a toast is shown over it with color "info", icon "i-lucide-info", title "Your account is
already verified", description "There's nothing to cancel."
And after 800 ms the app navigates to /home
Scenario Outline: Cancel failed — the dialog stays open and shows the failure inline
Given the ConfirmDialog's action is in flight after the user confirmed
When DELETE /v1/auth/register <failure>
Then the dialog does not close
And the failure is shown inline in the dialog, not as a toast
And both dialog buttons are re-enabled so the user can retry "Cancel registration" or choose
"Keep my account"
Examples:
| failure |
| fails with a 500 response |
| never reaches the server |