fix(GRO-2099): show loading state during CustomerPortal SSO bridge bootstrap
Root cause: `Dashboard.tsx:194` runs its own `!sessionId && !isImpersonating && !getDevUser()` auth guard, redirecting to `/login` if `sessionId` is null. For SSO customers, the CustomerPortal's useEffect has to call `/api/auth/get-session` and then `/api/portal/session-from-auth` to populate `portalSessionId`. During that bootstrap window (typically 100-300ms), `sessionId` is null and the guard fires — redirecting the user to `/login` and breaking the post-sign-in flow. App.tsx additionally returned `null` at `/login` for authenticated users (`showCustomerPortal` is false at `/login`), leaving a blank React root even if the redirect target was /login itself. Fix: - `CustomerPortal.tsx`: show a 'Loading…' state (`role=status`) while `!initComplete`. The portal chrome and its child sections only mount once the bootstrap has resolved, so child auth guards don't fire prematurely. - `App.tsx`: at `/login` with a valid session, redirect to `/` so the customer lands on the portal instead of seeing a blank page. - `App.tsx`: only return `LoginPage` when at `/login` — other portal routes defer the auth check to `CustomerPortal` (the customer SSO bridge resolves `portalSessionId` on mount). - `UAT_PLAYBOOK.md`: add §5.27 with 8 cases covering the bug, the loading state, the /login auto-redirect, the unauth fallback, and the groomer / impersonation non-regressions. - `src/__tests__/portal.test.tsx`: add a regression test that asserts the loading state is shown during the bridge and the portal nav is NOT in the DOM mid-bootstrap. Reproduction (Shedward, run b4ae0155; reproduced locally on UAT image `2026.06.01-ec29f71`): 1. From `about:blank`, complete customer SSO as `uat-customer`. 2. `browser_navigate` to `/portal`. 3. Pre-fix: redirected to `/login` with blank React root. 4. Post-fix: URL stays at `/portal`, dashboard renders with customer name. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -377,6 +377,26 @@ These cases cover the `CustomerPortal` initialisation path that bridges an Authe
|
||||
| TC-WEB-5.25.10 | Unauthenticated user is sent to login (no infinite loop) | Without signing in, navigate directly to `/`. | `App.tsx` renders the LoginPage. `CustomerPortal` does not render. No `session-from-auth` request is made. |
|
||||
| TC-WEB-5.25.11 | Session persists across reload via Better Auth cookie | After TC-WEB-5.25.1 succeeds, reload the page. | Portal dashboard re-renders. A fresh `GET /api/auth/get-session` + `POST /api/portal/session-from-auth` pair runs and yields 200/201. Greeting still reads "Hi, <FirstName>". |
|
||||
|
||||
### 5.27 Customer Portal — Authenticated HTML-route cold mount (GRO-2099)
|
||||
|
||||
These cases guard against the regression where a customer who had just completed SSO sign-in was bounced back to `/login` (with a blank React root) when navigating directly to `/portal`, `/book`, `/schedule`, or even `/login` itself. Root cause: `Dashboard.tsx`'s `!sessionId && !isImpersonating && !getDevUser()` guard fired during the CustomerPortal's bootstrap — before the SSO bridge resolved `portalSessionId` — and redirected to `/login`. The fix: `CustomerPortal` now shows a loading state while the bootstrap is in flight, so the portal chrome and its `!sessionId` child guards do not mount prematurely. App.tsx additionally redirects an authenticated user at `/login` to `/` instead of rendering `null`.
|
||||
|
||||
**Pre-conditions:**
|
||||
|
||||
- TC-WEB-5.25.1 — TC-WEB-5.25.3 must pass on the build under test.
|
||||
- Clear cookies and localStorage between cases.
|
||||
|
||||
| # | Scenario | Steps | Expected |
|
||||
|---|----------|-------|----------|
|
||||
| TC-WEB-5.27.1 | Authenticated customer lands on `/portal` after direct nav | 1. From clean state, complete TC-WEB-5.25.1 (SSO sign-in as a customer). 2. Land on `/`. 3. `browser_navigate` (full page load) directly to `/portal`. | Final URL stays at `/portal`. The React root is non-empty. The portal dashboard renders with the customer's name. No `Navigate to /login` fires. |
|
||||
| TC-WEB-5.27.2 | Authenticated customer lands on `/book` and `/schedule` after direct nav | From TC-WEB-5.27.1, `browser_navigate` to `/book` then `/schedule` (one fresh navigation each). | Each final URL stays at the navigated path. The portal chrome is visible. The page does not redirect to `/login`. |
|
||||
| TC-WEB-5.27.3 | Authenticated customer at `/login` is auto-redirected to `/` | From TC-WEB-5.27.1, `browser_navigate` to `/login`. | The browser ends at `/` (not at a blank `/login`). The portal dashboard renders. No blank React root at `/login`. |
|
||||
| TC-WEB-5.27.4 | Loading state is visible during the bootstrap, no portal chrome flash | 1. With the UAT build under test, open DevTools → Network and throttle to Slow 3G. 2. Sign in via SSO. 3. Land on `/`. | A "Loading…" element (`role="status"`) is briefly visible. The portal nav (Home / Appointments / etc.) is NOT visible during the loading window. No `Navigate to /login` fires during the bootstrap. |
|
||||
| TC-WEB-5.27.5 | SSO bridge still runs and yields 201 | From TC-WEB-5.27.4 (or TC-WEB-5.27.1), inspect Network. | The same `GET /api/auth/get-session` (200) → `POST /api/portal/session-from-auth` (201) sequence from TC-WEB-5.25.2 still runs. The customer name appears in the greeting. |
|
||||
| TC-WEB-5.27.6 | Unauthenticated direct nav to `/portal` still ends at `/login` (no regression) | Clear cookies. `browser_navigate` to `/portal`. | The portal briefly shows the loading state, then `CustomerPortal`'s `!session && !portalSessionId` guard redirects to `/login`. The login form renders. No infinite loop. |
|
||||
| TC-WEB-5.27.7 | Groomer SSO still works (no regression) | 1. From clean state, sign in via SSO as the groomer identity (uat-groomer). 2. Land on `/`. | `App.tsx`'s staff check redirects to `/admin`. The groomer nav renders. No `CustomerPortal` flash. No `/portal` redirect loop. |
|
||||
| TC-WEB-5.27.8 | Impersonation session still works (no regression) | 1. With an active impersonation session, open `/?sessionId=<id>`. | The amber "STAFF VIEW" chrome renders. The portal loads. No `/login` redirect. |
|
||||
|
||||
### 5.26 Customer Portal — RescheduleFlow under SSO Bridge (GRO-2012)
|
||||
|
||||
These cases guard against the regression where an SSO-bridge customer (no `?sessionId=` URL param, no impersonation session) could trigger the RescheduleFlow and have `RescheduleFlow` receive `sessionId={null}`, which caused the internal `/api/book/availability` call to send `X-Impersonation-Session-Id: ` (empty) and return 401. The fix: `CustomerPortal` now passes `sessionId={session?.id ?? portalSessionId}` to `<RescheduleFlow>` (matching the fallback `renderSection()` already used).
|
||||
|
||||
Reference in New Issue
Block a user