Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/descope-auth-addon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tanstack/create": minor
---

Add Descope authentication add-on for React. Wires `<AuthProvider>` at the app root via `VITE_DESCOPE_PROJECT_ID`, a header user menu, and a `/demo/descope` route rendering the `sign-up-or-in` flow. Mutually exclusive with other auth providers.
45 changes: 45 additions & 0 deletions packages/create/src/frameworks/react/add-ons/descope/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## Setting up Descope

1. Sign up at [descope.com](https://descope.com) and create a project
2. Copy your **Project ID** from the [Console](https://app.descope.com/settings/project)
3. Set it in your `.env.local`:
```bash
VITE_DESCOPE_PROJECT_ID=P...
```
4. Make sure a **`sign-up-or-in`** flow exists in the Console (it ships by default)
5. Add `http://localhost:3000` to your project's approved domains / redirect URLs
6. Visit the demo route at `/demo/descope` once `npm run dev` is running

### What's wired up

- **`<AuthProvider>`** at the app root (`src/integrations/descope/provider.tsx`) supplies auth context to the whole tree
- **Header user menu** (`src/integrations/descope/header-user.tsx`) swaps between a "Sign in" link and the signed-in avatar + "Sign out"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Header user menu does not render a "Sign in" link.

The README states the header user menu "swaps between a 'Sign in' link and the signed-in avatar + 'Sign out'". However, header-user.tsx returns null when !isAuthenticated — there is no "Sign in" link. Update the documentation to match the actual behavior.

-- **Header user menu** (`src/integrations/descope/header-user.tsx`) swaps between a "Sign in" link and the signed-in avatar + "Sign out"
+- **Header user menu** (`src/integrations/descope/header-user.tsx`) renders the signed-in avatar + "Sign out" button (hidden when signed out)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **Header user menu** (`src/integrations/descope/header-user.tsx`) swaps between a "Sign in" link and the signed-in avatar + "Sign out"
- **Header user menu** (`src/integrations/descope/header-user.tsx`) renders the signed-in avatar + "Sign out" button (hidden when signed out)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/create/src/frameworks/react/add-ons/descope/README.md` at line 16,
The README description for the Descope header user menu is inaccurate because
`header-user.tsx` does not render a “Sign in” link when unauthenticated. Update
the documentation text for the `header-user` integration so it matches the
actual `HeaderUser` behavior: it returns nothing when `!isAuthenticated` and
only shows the avatar and “Sign out” state when signed in.

- **`/demo/descope`** renders the Descope `<Descope flowId="sign-up-or-in" />` flow and a signed-in greeting

### Protecting a route

Use the `useSession` hook to gate content:

```tsx
import { useSession } from '@descope/react-sdk'

function ProtectedPage() {
const { isAuthenticated, isSessionLoading } = useSession()

if (isSessionLoading) return <p>Loading…</p>
if (!isAuthenticated) return <RedirectToSignIn />

return <YourPageContent />
}
```

For server-side checks (route loaders, server functions), validate the session
token with the [`@descope/node-sdk`](https://docs.descope.com/sdks/nodejs) and
`validateSession()`.

### Production checklist

- Use a dedicated **production project** and its Project ID
- Configure your production domain under **Project Settings → Domains**
- Customize your flows (branding, connectors, MFA) in the **Flows** section of the Console
- Enable the social / SSO connectors you need under **Authentication Methods**
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Descope configuration, get your Project ID from the [Console](https://app.descope.com/settings/project)
VITE_DESCOPE_PROJECT_ID=
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useDescope, useSession, useUser } from '@descope/react-sdk'

export default function HeaderUser() {
const { isAuthenticated, isSessionLoading } = useSession()
const { user } = useUser()
const sdk = useDescope()

if (isSessionLoading || !isAuthenticated) return null

const email = user?.email
const initial = (user?.name || email || 'U').charAt(0).toUpperCase()
Comment on lines +8 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the `@descope/react-sdk` type definitions for useUser return shape
fd -t f 'react-sdk' -e d.ts node_modules/@descope 2>/dev/null | head -5
# If not found in node_modules, check the package's exported types
rg -n 'useUser' --type=ts -A5 packages/create/src/frameworks/react/add-ons/descope/ 2>/dev/null

Repository: TanStack/cli

Length of output: 2891


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the local component and any nearby docs or examples for Descope hook usage.
sed -n '1,120p' packages/create/src/frameworks/react/add-ons/descope/assets/src/integrations/descope/header-user.tsx
printf '\n---\n'
sed -n '1,140p' packages/create/src/frameworks/react/add-ons/descope/assets/src/routes/demo/descope.tsx
printf '\n---\n'
rg -n 'isUserLoading|useUser\(|useSession\(' packages/create/src/frameworks/react/add-ons/descope -A3 -B3

# Try to locate installed type definitions or source for `@descope/react-sdk` in the repo/cache.
printf '\n---\n'
fd -a 'react-sdk' . 2>/dev/null | head -50
printf '\n---\n'
fd -a 'descope' . 2>/dev/null | head -50

Repository: TanStack/cli

Length of output: 7987


🌐 Web query:

@descope/react-sdk useUser loading state isUserLoading useSession user loading documentation

💡 Result:

In the Descope React SDK, the hooks useSession and useUser are used to manage authentication and user data. Both hooks provide specific boolean flags to track loading states, which are essential for handling UI transitions while the SDK initializes or refreshes authentication status [1][2][3]. Key Loading State Hooks: - useSession: Returns isSessionLoading (a boolean) to indicate whether the session state is currently being fetched or validated [1][2][4]. - useUser: Returns isUserLoading (a boolean) to indicate whether user profile information is currently being fetched [1][3]. Best Practices for Implementation: - Usage within AuthProvider: These hooks must be used within an AuthProvider context [1][2]. If used outside of this context, they will throw an error [1][2]. - Conditional Rendering: You should check both isSessionLoading and isUserLoading before rendering protected content to ensure the application waits for the authentication process to complete [1][2][3]. Example Pattern: const { isAuthenticated, isSessionLoading } = useSession; const { user, isUserLoading } = useUser; if (isSessionLoading || isUserLoading) { return

Loading...
; // Render a loading indicator } if (!isAuthenticated) { return
Please log in
; // Handle unauthenticated state } return
Welcome, {user.name}
; // Render authenticated content Important Technical Consideration: Historical versions of the SDK (and potentially certain environments) may face issues where loading states remain true indefinitely if network requests fail during a proactive refresh [5]. Ensuring you are using a recent version of the SDK helps mitigate these issues, as updates have been implemented to ensure loading states are cleared regardless of whether the fetch succeeds or fails [5]. Note on variations: Depending on the specific version or wrapper (such as @descope/react-sdk vs @descope/nextjs-sdk), you may occasionally see different naming conventions like isLoading instead of isSessionLoading in some documentation examples [6], but isSessionLoading and isUserLoading are the standard, widely-documented properties for these specific hooks in current releases [1][2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the installed package version if present in lockfiles.
rg -n '"`@descope/react-sdk`"|`@descope/react-sdk`' package.json package-lock.json pnpm-lock.yaml yarn.lock npm-shrinkwrap.json -A2 -B2 || true

# Look for any vendored type definitions or docs in the repo that mention useUser fields.
rg -n 'isUserLoading|userLoading|useUser.*loading|useUser' . -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**' -A3 -B3 || true

# If the package is installed in the sandbox cache, inspect exported declarations without executing repo code.
python3 - <<'PY'
import os, glob
candidates = []
for base in [os.getcwd(), '/tmp', '/workspace', '/root/.npm', '/root/.cache']:
    for p in glob.glob(base + '/**/@descope/react-sdk*', recursive=True):
        if os.path.isdir(p):
            candidates.append(p)
print('\n'.join(candidates[:50]))
PY

Repository: TanStack/cli

Length of output: 5146


🌐 Web query:

site:descope.com/docs/react-sdk useUser isUserLoading useSession isAuthenticated

💡 Result:

In Descope React SDK:

  • useSession() returns isAuthenticated and isSessionLoading. (docs.descope.com)
  • useUser() returns user and isUserLoading. isUserLoading means the user object is still loading. (docs.descope.com)
  • Both hooks must be used inside AuthProvider. (docs.descope.com)

Typical usage:

const { isAuthenticated, isSessionLoading } = useSession()
const { user, isUserLoading } = useUser()

Also, Descope notes that useSession() triggers a session refresh request automatically. (docs.descope.com)

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,200p' packages/create/src/frameworks/react/add-ons/descope/assets/src/integrations/descope/header-user.tsx

Repository: TanStack/cli

Length of output: 1264


Gate the avatar on isUserLoading as well. useUser() exposes a loading flag, so this component can render the 'U' fallback before the profile arrives; wait for user loading to finish before showing the initial.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/create/src/frameworks/react/add-ons/descope/assets/src/integrations/descope/header-user.tsx`
around lines 8 - 11, The avatar initial in header-user.tsx is rendered before
the user profile has finished loading, which can show the fallback too early.
Update the HeaderUser component to also guard on isUserLoading alongside
isSessionLoading and isAuthenticated, and only compute/render the initial after
user loading completes so the `useUser()` data is available before using
`user?.name` or `user?.email`.


return (
<div className="flex items-center gap-2">
{user?.picture ? (
<img src={user.picture} alt="" className="h-8 w-8 rounded-full" />
) : (
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-neutral-200 dark:bg-neutral-800">
<span className="text-xs font-medium text-neutral-600 dark:text-neutral-400">
{initial}
</span>
</div>
)}
<button
type="button"
onClick={() => sdk.logout()}
className="rounded-xl px-3 py-1.5 text-sm font-semibold text-[var(--sea-ink-soft)] transition hover:bg-[var(--link-bg-hover)] hover:text-[var(--sea-ink)]"
>
Sign out
</button>
Comment on lines +24 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File outline ==\n'
ast-grep outline packages/create/src/frameworks/react/add-ons/descope/assets/src/integrations/descope/header-user.tsx --view expanded || true

printf '\n== Relevant file contents ==\n'
cat -n packages/create/src/frameworks/react/add-ons/descope/assets/src/integrations/descope/header-user.tsx

printf '\n== Search for logout definitions/usages ==\n'
rg -n "logout\\(" packages/create/src/frameworks/react/add-ons/descope -g '!**/node_modules/**' || true

Repository: TanStack/cli

Length of output: 1873


🌐 Web query:

Descope React SDK logout() returns Promise logout documentation

💡 Result:

The logout method provided by the Descope React SDK (accessed via the useDescope hook) returns a Promise, allowing you to use async/await syntax to handle post-logout logic such as navigation [1]. While some documentation examples show the method called without awaiting it [2], it is standard practice and recommended to await the logout call to ensure the session has been successfully invalidated before proceeding with client-side actions, such as redirecting the user to a login page [1]. Example usage: const { logout } = useDescope; const navigate = useNavigate; const handleLogout = async => { await logout; navigate('/login'); };

Citations:


Handle the sdk.logout() promise
sdk.logout() returns a Promise, so the current click handler can drop a rejection on logout failure. Add a catch/await path and surface a failure state or fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/create/src/frameworks/react/add-ons/descope/assets/src/integrations/descope/header-user.tsx`
around lines 24 - 30, The Sign out click handler in header-user.tsx ignores the
Promise returned by sdk.logout(), so logout failures can become unhandled
rejections. Update the onClick path around sdk.logout() to await it or attach a
catch, and surface a visible fallback or error state from this component so
failures are handled cleanly.

</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { AuthProvider } from '@descope/react-sdk'

const PROJECT_ID = import.meta.env.VITE_DESCOPE_PROJECT_ID
if (!PROJECT_ID) {
throw new Error('Add your Descope Project ID to the .env.local file')
}

export default function AppDescopeProvider({
children,
}: {
children: React.ReactNode
}) {
return <AuthProvider projectId={PROJECT_ID}>{children}</AuthProvider>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { createFileRoute } from '@tanstack/react-router'
import { Descope, useSession, useUser } from '@descope/react-sdk'

export const Route = createFileRoute('/demo/descope')({
component: DescopeDemo,
})

function DescopeDemo() {
const { isAuthenticated, isSessionLoading } = useSession()

return (
<main className="demo-page demo-center">
<section className="demo-panel w-full max-w-md space-y-6">
{isSessionLoading ? (
<p className="demo-muted text-center text-sm">Loading…</p>
) : isAuthenticated ? (
<SignedInGreeting />
) : (
<SignedOut />
)}
</section>
</main>
)
}

function SignedOut() {
return (
<>
<div className="space-y-1.5">
<p className="island-kicker mb-2">Descope</p>
<h1 className="demo-title">Sign in to continue</h1>
<p className="demo-muted text-sm">
Descope renders the sign-in flow, manages sessions, and handles
passwordless, SSO, and social login for you.
</p>
</div>
<div className="flex justify-center pt-2">
<Descope
flowId="sign-up-or-in"
theme="light"
onError={(err) => console.error('Descope flow error', err)}
/>
</div>
<p className="demo-muted text-center text-xs">
Built with{' '}
<a
href="https://descope.com"
target="_blank"
rel="noopener noreferrer"
className="font-medium"
>
DESCOPE
</a>
.
</p>
</>
)
}

function SignedInGreeting() {
const { user } = useUser()
if (!user) return null

const email = user.email
const initial = (user.name || email || 'U').charAt(0).toUpperCase()

return (
<div className="space-y-6">
<div className="space-y-1.5">
<p className="island-kicker mb-2">Descope</p>
<h1 className="demo-title">Welcome back</h1>
<p className="demo-muted text-sm">You're signed in as {email}</p>
</div>

<div className="flex items-center gap-3">
{user.picture ? (
<img src={user.picture} alt="" className="h-10 w-10 rounded-full" />
) : (
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-neutral-200 dark:bg-neutral-800">
<span className="text-sm font-medium text-neutral-600 dark:text-neutral-400">
{initial}
</span>
</div>
)}
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{user.name}</p>
<p className="truncate text-xs text-neutral-500 dark:text-neutral-400">
{email}
</p>
</div>
</div>

<p className="demo-muted text-center text-xs">
Sign out from the avatar in the header. Built with{' '}
<a
href="https://descope.com"
target="_blank"
rel="noopener noreferrer"
className="font-medium"
>
DESCOPE
</a>
.
</p>
</div>
)
}
42 changes: 42 additions & 0 deletions packages/create/src/frameworks/react/add-ons/descope/info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "Descope",
"description": "Drop-in auth with hosted flows, passwordless, SSO, and social login (managed).",
"phase": "add-on",
"modes": ["file-router"],
"type": "add-on",
"category": "auth",
"exclusive": ["auth"],
"color": "#0081B3",
"priority": 170,
"link": "https://descope.com",
"routes": [
{
"icon": "CircleUserRound",
"url": "/demo/descope",
"name": "Descope",
"path": "src/routes/demo.descope.tsx",
"jsName": "DescopeDemo"
}
],
"integrations": [
{
"type": "header-user",
"jsName": "DescopeHeader",
"path": "src/integrations/descope/header-user.tsx"
},
{
"type": "provider",
"jsName": "DescopeProvider",
"path": "src/integrations/descope/provider.tsx"
}
],
"envVars": [
{
"name": "VITE_DESCOPE_PROJECT_ID",
"description": "Descope project ID",
"required": true,
"secret": false,
"file": ".env.local"
}
]
}
34 changes: 34 additions & 0 deletions packages/create/src/frameworks/react/add-ons/descope/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"@descope/react-sdk": "^2.30.3"
}
}
Loading