feat: register T1 (pending, verify)
This commit is contained in:
1 parent
3c3f7c6194
commit
4e41d60142
29 files changed
+2162
-42
No files matched your search
@@ -0,0 +1,152 @@
|
||||
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, the app
|
||||
navigates to /home, and a toast (color "info", icon "i-lucide-info", title "Your account is
|
||||
already verified", description "There's nothing to cancel.") is shown; PendingPage does not
|
||||
render a ResultPanel for this case, since the ResultPanel drawn in this state on the design
|
||||
board is Verify's own Success panel, reused purely to depict the destination page behind the
|
||||
toast
|
||||
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 refreshes the session and navigates to /home with a toast
|
||||
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 the app navigates to /home
|
||||
And a toast is shown with color "info", icon "i-lucide-info", title "Your account is already
|
||||
verified", description "There's nothing to cancel."
|
||||
And no ResultPanel is rendered by PendingPage for this transition
|
||||
|
||||
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 |
|
||||
Reference in new issue
Block a user