Skip to content

fix(AuthProvider): split guardComponent into loadingComponent + guardComponent#269

Closed
erickteowarang wants to merge 9 commits into
mainfrom
feat/auth/loading-component
Closed

fix(AuthProvider): split guardComponent into loadingComponent + guardComponent#269
erickteowarang wants to merge 9 commits into
mainfrom
feat/auth/loading-component

Conversation

@erickteowarang
Copy link
Copy Markdown
Contributor

@erickteowarang erickteowarang commented May 18, 2026

Summary

  • AuthProvider's single guardComponent slot rendered for the union of !isReady and !isAuthenticated. Any sign-in screen wired into that slot — the obvious thing to do, and what the type name implies — flashes on every reload before the session is known. This contradicts the canonical pattern documented in useAuth's own JSDoc.
  • Add loadingComponent for the !isReady state. Narrow guardComponent to fire only when isReady && !isAuthenticated (sign-in screens no longer flash).
  • OAuth callback child-suppression now keys off loadingComponent — the slot that explicitly owns the !isReady state.
  • Default flash protection: when autoLogin is enabled and a slot is omitted, the protected tree is hidden during that state instead of briefly rendering children. When autoLogin is off, children render in those windows — preserving the useAuthSuspense pattern (where a <Suspense> boundary inside the tree owns the loading UI) and public-app cases.

Demo here: https://www.loom.com/share/d71ac5293e3d45039e795bd675fa8abb

Default behavior matrix (no slots passed)

autoLogin !isReady isReady && !isAuthenticated
true hidden hidden
false / unset children render children render

Migration

// Before — guardComponent doubled as loading + unauthenticated
<AuthProvider client={authClient} guardComponent={() => <LoadingScreen />}>

// After — use the slot that matches your intent
<AuthProvider
  client={authClient}
  loadingComponent={() => <LoadingScreen />}
  guardComponent={() => <SignInScreen />}
>

// Or, with autoLogin, omit both and rely on default flash protection
<AuthProvider client={authClient} autoLogin>

If you were passing a loading UI to guardComponent, rename to loadingComponent. If you were passing a sign-in screen, keep it on guardComponent — it will no longer flash before the auth check resolves.

Test plan

  • pnpm type-check clean
  • pnpm lint 0 warnings / 0 errors
  • pnpm test — 1015 tests pass, including:
    • New regression: guardComponent does NOT render during !isReady
    • Renamed: loadingComponent renders during !isReady
    • Reframed: callback-pending suppression triggers off loadingComponent
    • New: autoLogin hides children during !isReady when no loadingComponent is set
    • New: autoLogin hides children during !isAuthenticated when no guardComponent is set
    • New: without autoLogin, children render during !isReady (Suspense pattern preserved)
  • pnpm fmt applied
  • Changeset added (.changeset/auth-loading-component.md, minor)
  • Docs updated (docs/concepts/authentication.md)

🤖 Generated with Claude Code

…Component

Previously `guardComponent` rendered for the union of `!isReady` and
`!isAuthenticated`, so any sign-in screen wired into that slot would
flash on every reload before the session was known — contradicting the
canonical `useAuth` pattern documented in the SDK itself.

Add `loadingComponent` for the `!isReady` state and narrow
`guardComponent` to fire only when `isReady && !isAuthenticated`. OAuth
callback child-suppression now keys off `loadingComponent` (the slot
that owns the `!isReady` state).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@IzumiSy
Copy link
Copy Markdown
Contributor

IzumiSy commented May 18, 2026

/review

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by API Design Review for issue #269

Comment thread packages/core/src/contexts/auth-context.tsx Outdated
Comment thread packages/core/src/contexts/auth-context.tsx Outdated
…gin is on

`loadingComponent` and `guardComponent` are both optional. Previously,
omitting one meant children rendered through during that state — which,
combined with `autoLogin`, briefly showed protected UI before the
redirect fired.

Couple the "hide on omission" default to `autoLogin`: when it is on, an
unset slot renders nothing so protected UI never flashes; when it is
off, children continue to render so the `useAuthSuspense` and public-app
patterns still work without any new props.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 18, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@tailor-platform/app-shell@269
npm i https://pkg.pr.new/@tailor-platform/app-shell-sdk-plugin@269
npm i https://pkg.pr.new/@tailor-platform/app-shell-vite-plugin@269

commit: 108c64a

Erick Teowarang and others added 2 commits May 18, 2026 16:23
`pnpm fmt:check` failed CI on the props table. Also point the
`useAuthSuspense` cross-reference at the correct heading slug
(`#suspense-compatible-hook`, not `#suspense-integration`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous callback suppression set `resolvedChildren` to `null` but
still rendered `AuthGuard`. If the auth client surfaced a stale
`isReady && !isAuthenticated` state while a callback exchange was in
flight, `AuthGuard` would render `guardComponent` (the sign-in screen) —
flashing the very UI the user just came back from.

Bypass `AuthGuard` entirely during the blackout window. Providing
`loadingComponent` still opts the consumer out of the blackout, so the
"trust me, I'll handle transitions" contract is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread packages/core/src/contexts/auth-context.tsx Outdated
Erick Teowarang and others added 5 commits May 19, 2026 15:55
…ok scope

Invoking `loadingComponent()` / `guardComponent()` as plain function calls
inlined the slot's hooks into AuthGuard's own hook list. Because the
calls are gated on auth state, the hook order changed across renders
the moment a consumer passed a slot that used a hook itself (e.g. a
sign-in screen calling `useAuth`) — tripping React's rules of hooks.

Render each slot via `createElement` so it becomes its own fiber. The
existing tests missed this because their slot components were stateless;
add a regression test that asserts no hook-order warnings when the slot
calls `useAuth` and the auth state transitions out of it.

Reported by IzumiSy in #269.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend the auth Playwright suite with a regression test that walks the
full login flow and asserts:
- the auth guard is not in the DOM after the callback resolves
  (no sign-in flash during the OAuth callback exchange), and
- no React hook-order warnings are logged during the auth state
  transition (the E2E app's <AuthGuard> uses `useAuth`, the exact
  shape that previously inlined a hook into AuthGuard's hook list).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to the existing "maintains session on page reload" test:
that one verifies the authenticated content is eventually visible
after reload, but does not catch a single-frame guardComponent flash
during the `!isReady` window.

Inject a MutationObserver via `addInitScript` so it is attached
before any post-reload render, and assert the auth-guard testid
never enters the DOM while the session is being re-checked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@erickteowarang
Copy link
Copy Markdown
Contributor Author

Closing this as I've determined that the original issue is more related to DT and erp-kit than app-shell

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants