-
-
Notifications
You must be signed in to change notification settings - Fork 180
feat(create): add Descope auth add-on for React #483
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
| 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" | ||
| - **`/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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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/nullRepository: 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 -50Repository: TanStack/cli Length of output: 7987 🌐 Web query:
💡 Result: In the Descope React SDK, the hooks 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]))
PYRepository: TanStack/cli Length of output: 5146 🌐 Web query:
💡 Result: In Descope React SDK:
Typical usage: const { isAuthenticated, isSessionLoading } = useSession()
const { user, isUserLoading } = useUser()Also, Descope notes that 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.tsxRepository: TanStack/cli Length of output: 1264 Gate the avatar on 🤖 Prompt for AI Agents |
||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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/**' || trueRepository: TanStack/cli Length of output: 1873 🌐 Web query:
💡 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 🤖 Prompt for AI Agents |
||
| </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> | ||
| ) | ||
| } |
| 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" | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "dependencies": { | ||
| "@descope/react-sdk": "^2.30.3" | ||
| } | ||
| } |
There was a problem hiding this comment.
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.tsxreturnsnullwhen!isAuthenticated— there is no "Sign in" link. Update the documentation to match the actual behavior.📝 Committable suggestion
🤖 Prompt for AI Agents