From f5c003cb4ffc5f2ee05457a5e815b40f16dfe030 Mon Sep 17 00:00:00 2001 From: Maria Mantsurova Date: Wed, 8 Jul 2026 14:24:40 +0300 Subject: [PATCH 1/3] feat: add Descope --- docs/start/config.json | 4 + docs/start/framework/react/getting-started.md | 1 + .../.devcontainer/devcontainer.json | 3 + examples/react/start-descope/.env.example | 11 + examples/react/start-descope/.gitignore | 20 + examples/react/start-descope/.prettierignore | 4 + .../react/start-descope/.vscode/settings.json | 11 + examples/react/start-descope/README.md | 76 +++ examples/react/start-descope/package.json | 33 ++ .../src/components/DefaultCatchBoundary.tsx | 51 ++ .../src/components/DescopeLogo.tsx | 108 +++++ .../start-descope/src/components/NotFound.tsx | 25 + .../src/integrations/descope/middleware.ts | 20 + .../src/integrations/descope/provider.tsx | 21 + .../src/integrations/descope/server.ts | 32 ++ .../integrations/descope/session.server.ts | 72 +++ .../react/start-descope/src/routeTree.gen.ts | 147 ++++++ examples/react/start-descope/src/router.tsx | 17 + .../react/start-descope/src/routes/__root.tsx | 139 ++++++ .../start-descope/src/routes/_authed.tsx | 11 + .../src/routes/_authed/profile.tsx | 84 ++++ .../react/start-descope/src/routes/index.tsx | 236 ++++++++++ .../react/start-descope/src/routes/login.tsx | 54 +++ .../react/start-descope/src/routes/logout.tsx | 37 ++ examples/react/start-descope/src/start.ts | 22 + .../react/start-descope/src/styles/app.css | 36 ++ examples/react/start-descope/src/utils/seo.ts | 33 ++ examples/react/start-descope/tsconfig.json | 21 + examples/react/start-descope/vite.config.ts | 14 + pnpm-lock.yaml | 434 ++++++++++++++++++ pnpm-workspace.yaml | 1 + 31 files changed, 1778 insertions(+) create mode 100644 examples/react/start-descope/.devcontainer/devcontainer.json create mode 100644 examples/react/start-descope/.env.example create mode 100644 examples/react/start-descope/.gitignore create mode 100644 examples/react/start-descope/.prettierignore create mode 100644 examples/react/start-descope/.vscode/settings.json create mode 100644 examples/react/start-descope/README.md create mode 100644 examples/react/start-descope/package.json create mode 100644 examples/react/start-descope/src/components/DefaultCatchBoundary.tsx create mode 100644 examples/react/start-descope/src/components/DescopeLogo.tsx create mode 100644 examples/react/start-descope/src/components/NotFound.tsx create mode 100644 examples/react/start-descope/src/integrations/descope/middleware.ts create mode 100644 examples/react/start-descope/src/integrations/descope/provider.tsx create mode 100644 examples/react/start-descope/src/integrations/descope/server.ts create mode 100644 examples/react/start-descope/src/integrations/descope/session.server.ts create mode 100644 examples/react/start-descope/src/routeTree.gen.ts create mode 100644 examples/react/start-descope/src/router.tsx create mode 100644 examples/react/start-descope/src/routes/__root.tsx create mode 100644 examples/react/start-descope/src/routes/_authed.tsx create mode 100644 examples/react/start-descope/src/routes/_authed/profile.tsx create mode 100644 examples/react/start-descope/src/routes/index.tsx create mode 100644 examples/react/start-descope/src/routes/login.tsx create mode 100644 examples/react/start-descope/src/routes/logout.tsx create mode 100644 examples/react/start-descope/src/start.ts create mode 100644 examples/react/start-descope/src/styles/app.css create mode 100644 examples/react/start-descope/src/utils/seo.ts create mode 100644 examples/react/start-descope/tsconfig.json create mode 100644 examples/react/start-descope/vite.config.ts diff --git a/docs/start/config.json b/docs/start/config.json index e93688eb91..8d5214d39b 100644 --- a/docs/start/config.json +++ b/docs/start/config.json @@ -369,6 +369,10 @@ "label": "WorkOS", "to": "framework/react/examples/start-workos" }, + { + "label": "Descope", + "to": "framework/react/examples/start-descope" + }, { "label": "Material UI", "to": "framework/react/examples/start-material-ui" diff --git a/docs/start/framework/react/getting-started.md b/docs/start/framework/react/getting-started.md index 565dc4b4cf..b3c7e0b001 100644 --- a/docs/start/framework/react/getting-started.md +++ b/docs/start/framework/react/getting-started.md @@ -47,6 +47,7 @@ To use a different example, replace `start-basic` with one of the slugs below. - [Supabase](https://github.com/TanStack/router/tree/main/examples/react/start-supabase-basic) (`start-supabase-basic`) - [Trellaux](https://github.com/TanStack/router/tree/main/examples/react/start-trellaux) (`start-trellaux`) - [WorkOS](https://github.com/TanStack/router/tree/main/examples/react/start-workos) (`start-workos`) +- [Descope](https://github.com/TanStack/router/tree/main/examples/react/start-descope) (`start-descope`) - [Material UI](https://github.com/TanStack/router/tree/main/examples/react/start-material-ui) (`start-material-ui`) Each example page also includes a StackBlitz preview so you can inspect it before cloning. diff --git a/examples/react/start-descope/.devcontainer/devcontainer.json b/examples/react/start-descope/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..e5dafce188 --- /dev/null +++ b/examples/react/start-descope/.devcontainer/devcontainer.json @@ -0,0 +1,3 @@ +{ + "image": "mcr.microsoft.com/devcontainers/typescript-node:24" +} diff --git a/examples/react/start-descope/.env.example b/examples/react/start-descope/.env.example new file mode 100644 index 0000000000..2d8ad7f80b --- /dev/null +++ b/examples/react/start-descope/.env.example @@ -0,0 +1,11 @@ +# Your Descope Project ID (found at https://app.descope.com/settings/project). +# Exposed to the browser for the client-side auth flow, so it must be VITE_ prefixed. +VITE_DESCOPE_PROJECT_ID= + +# The same Project ID, read by the server to validate sessions. +DESCOPE_PROJECT_ID= + +# Optional. Only needed for a custom domain (CNAME), a self-hosted Descope +# instance, or a non-default region (e.g. the EU cluster: https://api.euc1.descope.com). +# VITE_DESCOPE_BASE_URL= +# DESCOPE_BASE_URL= diff --git a/examples/react/start-descope/.gitignore b/examples/react/start-descope/.gitignore new file mode 100644 index 0000000000..cd1bb463bd --- /dev/null +++ b/examples/react/start-descope/.gitignore @@ -0,0 +1,20 @@ +node_modules +# Keep environment variables out of version control +!.env.example +package-lock.json +yarn.lock + +.DS_Store +.cache +.env +.vercel +.output +/build/ +/api/ +/server/build +/public/build# Sentry Config File +.env.sentry-build-plugin +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/examples/react/start-descope/.prettierignore b/examples/react/start-descope/.prettierignore new file mode 100644 index 0000000000..40a96e8819 --- /dev/null +++ b/examples/react/start-descope/.prettierignore @@ -0,0 +1,4 @@ +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts diff --git a/examples/react/start-descope/.vscode/settings.json b/examples/react/start-descope/.vscode/settings.json new file mode 100644 index 0000000000..00b5278e58 --- /dev/null +++ b/examples/react/start-descope/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "files.watcherExclude": { + "**/routeTree.gen.ts": true + }, + "search.exclude": { + "**/routeTree.gen.ts": true + }, + "files.readonlyInclude": { + "**/routeTree.gen.ts": true + } +} diff --git a/examples/react/start-descope/README.md b/examples/react/start-descope/README.md new file mode 100644 index 0000000000..28a5aebc57 --- /dev/null +++ b/examples/react/start-descope/README.md @@ -0,0 +1,76 @@ +# TanStack Start + Descope + +A [TanStack Start](https://tanstack.com/start) example demonstrating how to authenticate users with [Descope](https://www.descope.com): the [`@descope/react-sdk`](https://www.npmjs.com/package/@descope/react-sdk) renders the sign-in flow on the client, and the [`@descope/node-sdk`](https://www.npmjs.com/package/@descope/node-sdk) validates sessions on the server during SSR. + +- [TanStack Router Docs](https://tanstack.com/router) +- [Descope Documentation](https://docs.descope.com) + +## Start a new project based on this example + +To start a new project based on this example, run: + +```sh +npx gitpick TanStack/router/tree/main/examples/react/start-descope start-descope +``` + +## Prerequisites + +You will need a [Descope account](https://www.descope.com/sign-up). Every project ships with a default `sign-up-or-in` flow, which is what this example renders — no extra flow setup is required. + +## Running the example + +1. Grab your **Project ID** from the [Descope console](https://app.descope.com/settings/project). +2. Copy `.env.example` to `.env` and fill in your Project ID: + + ```bash + cp .env.example .env + ``` + + ```bash + # Exposed to the browser for the client-side auth flow (must be VITE_ prefixed) + VITE_DESCOPE_PROJECT_ID= + # The same Project ID, read by the server to validate sessions + DESCOPE_PROJECT_ID= + ``` + + > [!NOTE] + > If you are using a custom domain (CNAME), or a private environment (for example `star`), also set `VITE_DESCOPE_BASE_URL` and `DESCOPE_BASE_URL`. + +3. Run the following command and navigate to [http://localhost:3000](http://localhost:3000). + + ```bash + pnpm dev + ``` + +## Build + +Build for production: + +```sh +pnpm build +``` + +## About This Example + +This example demonstrates: + +- Descope authentication via the hosted `sign-up-or-in` flow (``) +- Self-service account management via the `` widget (name, avatar, passkeys, MFA) +- Server-side session validation during SSR with `@descope/node-sdk`, wired through TanStack Start request middleware +- Protected routes guarded in `beforeLoad` +- User session management (login, logout, session refresh) + +### How it works + +The Descope integration lives in `src/integrations/descope/`: + +- **`provider.tsx`** — a client `` wrapping `` from `@descope/react-sdk`. Passing `sessionTokenViaCookie` stores the session token in a cookie so it is sent with the SSR document request (rather than living only in local storage). Used in `src/routes/__root.tsx`. +- **`middleware.ts`** — `descopeMiddleware()`, registered in `src/start.ts` as a TanStack Start `requestMiddleware`. It runs on every server request, validates/refreshes the session, and puts the user on the global start context. +- **`session.server.ts`** — the server-only logic: reads the `DS` (session) and `DSR` (refresh) cookies, calls `validateAndRefreshSession`, and persists a rotated session cookie. This is the only module that imports `@descope/node-sdk`. +- **`server.ts`** — a `getSession` server function that reads the user off the global context (works during SSR and client-side navigation). The root route's `beforeLoad` exposes it as `context.user`. + +Routes: + +- **Login** — `src/routes/login.tsx` renders the `` flow component. On success it invalidates the router so the server re-reads the new session. +- **Protected routes** — `src/routes/_authed.tsx` redirects to `/login` when there is no `context.user`; `src/routes/_authed/profile.tsx` renders the Descope `` widget (every project ships a default `user-profile-widget`) and shows the resolved session. +- **Logout** — `src/routes/logout.tsx` calls `useDescope().logout()` and clears the server cookies. diff --git a/examples/react/start-descope/package.json b/examples/react/start-descope/package.json new file mode 100644 index 0000000000..86c3029b49 --- /dev/null +++ b/examples/react/start-descope/package.json @@ -0,0 +1,33 @@ +{ + "name": "tanstack-start-example-descope", + "private": true, + "main": "index.js", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build && tsc --noEmit", + "preview": "vite preview", + "start": "pnpx srvx --prod -s ../client dist/server/server.js" + }, + "keywords": [], + "author": "", + "license": "MIT", + "dependencies": { + "@descope/node-sdk": "^2.12.0", + "@descope/react-sdk": "^2.30.3", + "@tanstack/react-router": "^1.170.17", + "@tanstack/react-router-devtools": "^1.167.0", + "@tanstack/react-start": "^1.168.27", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.2.2", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^6.0.1", + "tailwindcss": "^4.2.2", + "typescript": "^6.0.2", + "vite": "^8.0.14" + } +} diff --git a/examples/react/start-descope/src/components/DefaultCatchBoundary.tsx b/examples/react/start-descope/src/components/DefaultCatchBoundary.tsx new file mode 100644 index 0000000000..405fb5f4a7 --- /dev/null +++ b/examples/react/start-descope/src/components/DefaultCatchBoundary.tsx @@ -0,0 +1,51 @@ +import { + ErrorComponent, + Link, + useLocation, + useRouter, +} from '@tanstack/react-router' +import type { ErrorComponentProps } from '@tanstack/react-router' + +export function DefaultCatchBoundary({ error }: ErrorComponentProps) { + const router = useRouter() + const isRoot = useLocation({ + select: (location) => location.pathname === '/', + }) + + console.error(error) + + return ( +
+ +
+ + {isRoot ? ( + + Home + + ) : ( + { + e.preventDefault() + window.history.back() + }} + > + Go Back + + )} +
+
+ ) +} diff --git a/examples/react/start-descope/src/components/DescopeLogo.tsx b/examples/react/start-descope/src/components/DescopeLogo.tsx new file mode 100644 index 0000000000..acca9efb5d --- /dev/null +++ b/examples/react/start-descope/src/components/DescopeLogo.tsx @@ -0,0 +1,108 @@ +/** + * Official Descope logo (wordmark + mark), taken from descope.com. + * The wordmark uses `currentColor` so it adapts to light/dark mode; + * the mark keeps its brand gradient. + */ +export function DescopeLogo({ className }: { className?: string }) { + return ( + + + + + + + + + + + + + + + + + + + ) +} diff --git a/examples/react/start-descope/src/components/NotFound.tsx b/examples/react/start-descope/src/components/NotFound.tsx new file mode 100644 index 0000000000..22348fa94f --- /dev/null +++ b/examples/react/start-descope/src/components/NotFound.tsx @@ -0,0 +1,25 @@ +import { Link } from '@tanstack/react-router' + +export function NotFound({ children }: { children?: React.ReactNode }) { + return ( +
+
+ {children ||

The page you are looking for does not exist.

} +
+

+ + + Start Over + +

+
+ ) +} diff --git a/examples/react/start-descope/src/integrations/descope/middleware.ts b/examples/react/start-descope/src/integrations/descope/middleware.ts new file mode 100644 index 0000000000..d21f16181c --- /dev/null +++ b/examples/react/start-descope/src/integrations/descope/middleware.ts @@ -0,0 +1,20 @@ +import { createMiddleware } from '@tanstack/react-start' +import { DESCOPE_CONTEXT_KEY } from './server' +import { resolveSessionFromRequest } from './session.server' + +/** + * Request middleware that validates the Descope session once per request and + * puts the resulting user on the global start context. Register it in + * `src/start.ts`: + * + * ```ts + * export const startInstance = createStart(() => ({ + * requestMiddleware: [descopeMiddleware()], + * })) + * ``` + */ +export const descopeMiddleware = () => + createMiddleware().server(async ({ next }) => { + const user = await resolveSessionFromRequest() + return next({ context: { [DESCOPE_CONTEXT_KEY]: user } }) + }) diff --git a/examples/react/start-descope/src/integrations/descope/provider.tsx b/examples/react/start-descope/src/integrations/descope/provider.tsx new file mode 100644 index 0000000000..aabaf7d26a --- /dev/null +++ b/examples/react/start-descope/src/integrations/descope/provider.tsx @@ -0,0 +1,21 @@ +/// +import { AuthProvider } from '@descope/react-sdk' +import type { ReactNode } from 'react' + +/** + * Client-side Descope provider. `sessionTokenViaCookie` stores the session + * token in a cookie (rather than local storage) so it is sent with the SSR + * document request and can be validated on the server — see + * `src/integrations/descope/server.ts`. + */ +export function DescopeProvider({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/examples/react/start-descope/src/integrations/descope/server.ts b/examples/react/start-descope/src/integrations/descope/server.ts new file mode 100644 index 0000000000..07bc1999f9 --- /dev/null +++ b/examples/react/start-descope/src/integrations/descope/server.ts @@ -0,0 +1,32 @@ +import { createServerFn, getGlobalStartContext } from '@tanstack/react-start' + +// The key under which `descopeMiddleware` stores the resolved user on the +// global request context. +export const DESCOPE_CONTEXT_KEY = 'descopeUser' + +export interface SessionUser { + userId: string + email?: string + name?: string +} + +/** Reads the user resolved by `descopeMiddleware` off the global context. */ +function userFromContext(): SessionUser | null { + const ctx = getGlobalStartContext() as Record | undefined + return (ctx?.[DESCOPE_CONTEXT_KEY] as SessionUser | null | undefined) ?? null +} + +/** + * Server function exposing the current session to route `beforeLoad`. Works + * during SSR and during client-side navigation (via an RPC that re-runs the + * request middleware). + */ +export const getSession = createServerFn({ method: 'GET' }).handler( + async (): Promise => userFromContext(), +) + +/** Clears the Descope session cookies (used on logout). */ +export const clearServerSession = createServerFn().handler(async () => { + const { clearSessionCookies } = await import('./session.server') + clearSessionCookies() +}) diff --git a/examples/react/start-descope/src/integrations/descope/session.server.ts b/examples/react/start-descope/src/integrations/descope/session.server.ts new file mode 100644 index 0000000000..ea06db91f3 --- /dev/null +++ b/examples/react/start-descope/src/integrations/descope/session.server.ts @@ -0,0 +1,72 @@ +import DescopeClient from '@descope/node-sdk' +import { getCookies, setCookie } from '@tanstack/react-start/server' +import type { SessionUser } from './server' + +// Server-only module: reached from the request middleware and from server +// function handlers, never from the client bundle. It is the only place that +// imports `@descope/node-sdk` and `@tanstack/react-start/server`. + +// Descope stores the short-lived session token in the `DS` cookie and the +// refresh token in the `DSR` cookie by defaults. +const SESSION_COOKIE = DescopeClient.SessionTokenCookieName +const REFRESH_COOKIE = DescopeClient.RefreshTokenCookieName + +let descopeClient: ReturnType | undefined + +function getDescopeClient() { + if (!descopeClient) { + descopeClient = DescopeClient({ + projectId: process.env.DESCOPE_PROJECT_ID!, + // Only set for a self-hosted instance or a non-default region. + baseUrl: process.env.DESCOPE_BASE_URL, + }) + } + return descopeClient +} + +/** + * Reads the Descope cookies from the current request, validates the session + * (refreshing it if the session token has expired but a valid refresh token is + * present), and returns the authenticated user — or `null` if not signed in. + */ +export async function resolveSessionFromRequest(): Promise { + const cookies = getCookies() + const sessionToken = cookies[SESSION_COOKIE] + const refreshToken = cookies[REFRESH_COOKIE] + + if (!sessionToken && !refreshToken) { + return null + } + + try { + const authInfo = await getDescopeClient().validateAndRefreshSession( + sessionToken, + refreshToken, + ) + + // If the session token was rotated during refresh, persist the new value so + // the browser and any subsequent requests stay in sync. + if (authInfo.jwt && authInfo.jwt !== sessionToken) { + setCookie(SESSION_COOKIE, authInfo.jwt, { + httpOnly: false, + sameSite: 'lax', + path: '/', + }) + } + + const claims = authInfo.token + return { + userId: String(claims.sub), + email: typeof claims.email === 'string' ? claims.email : undefined, + name: typeof claims.name === 'string' ? claims.name : undefined, + } + } catch { + return null + } +} + +/** Clears the Descope session cookies (used on logout). */ +export function clearSessionCookies() { + setCookie(SESSION_COOKIE, '', { path: '/', maxAge: 0 }) + setCookie(REFRESH_COOKIE, '', { path: '/', maxAge: 0 }) +} diff --git a/examples/react/start-descope/src/routeTree.gen.ts b/examples/react/start-descope/src/routeTree.gen.ts new file mode 100644 index 0000000000..2a1d27911a --- /dev/null +++ b/examples/react/start-descope/src/routeTree.gen.ts @@ -0,0 +1,147 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as LogoutRouteImport } from './routes/logout' +import { Route as LoginRouteImport } from './routes/login' +import { Route as AuthedRouteImport } from './routes/_authed' +import { Route as IndexRouteImport } from './routes/index' +import { Route as AuthedProfileRouteImport } from './routes/_authed/profile' + +const LogoutRoute = LogoutRouteImport.update({ + id: '/logout', + path: '/logout', + getParentRoute: () => rootRouteImport, +} as any) +const LoginRoute = LoginRouteImport.update({ + id: '/login', + path: '/login', + getParentRoute: () => rootRouteImport, +} as any) +const AuthedRoute = AuthedRouteImport.update({ + id: '/_authed', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const AuthedProfileRoute = AuthedProfileRouteImport.update({ + id: '/profile', + path: '/profile', + getParentRoute: () => AuthedRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/login': typeof LoginRoute + '/logout': typeof LogoutRoute + '/profile': typeof AuthedProfileRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/login': typeof LoginRoute + '/logout': typeof LogoutRoute + '/profile': typeof AuthedProfileRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/_authed': typeof AuthedRouteWithChildren + '/login': typeof LoginRoute + '/logout': typeof LogoutRoute + '/_authed/profile': typeof AuthedProfileRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/login' | '/logout' | '/profile' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/login' | '/logout' | '/profile' + id: '__root__' | '/' | '/_authed' | '/login' | '/logout' | '/_authed/profile' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AuthedRoute: typeof AuthedRouteWithChildren + LoginRoute: typeof LoginRoute + LogoutRoute: typeof LogoutRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/logout': { + id: '/logout' + path: '/logout' + fullPath: '/logout' + preLoaderRoute: typeof LogoutRouteImport + parentRoute: typeof rootRouteImport + } + '/login': { + id: '/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginRouteImport + parentRoute: typeof rootRouteImport + } + '/_authed': { + id: '/_authed' + path: '' + fullPath: '/' + preLoaderRoute: typeof AuthedRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/_authed/profile': { + id: '/_authed/profile' + path: '/profile' + fullPath: '/profile' + preLoaderRoute: typeof AuthedProfileRouteImport + parentRoute: typeof AuthedRoute + } + } +} + +interface AuthedRouteChildren { + AuthedProfileRoute: typeof AuthedProfileRoute +} + +const AuthedRouteChildren: AuthedRouteChildren = { + AuthedProfileRoute: AuthedProfileRoute, +} + +const AuthedRouteWithChildren = + AuthedRoute._addFileChildren(AuthedRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AuthedRoute: AuthedRouteWithChildren, + LoginRoute: LoginRoute, + LogoutRoute: LogoutRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { startInstance } from './start.ts' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + config: Awaited> + } +} diff --git a/examples/react/start-descope/src/router.tsx b/examples/react/start-descope/src/router.tsx new file mode 100644 index 0000000000..c19f9c01ec --- /dev/null +++ b/examples/react/start-descope/src/router.tsx @@ -0,0 +1,17 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + const router = createRouter({ + routeTree, + scrollRestoration: true, + }) + + return router +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/examples/react/start-descope/src/routes/__root.tsx b/examples/react/start-descope/src/routes/__root.tsx new file mode 100644 index 0000000000..9c2ef372b1 --- /dev/null +++ b/examples/react/start-descope/src/routes/__root.tsx @@ -0,0 +1,139 @@ +/// +import { + HeadContent, + Link, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' +import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' +import * as React from 'react' +import { DefaultCatchBoundary } from '../components/DefaultCatchBoundary' +import { NotFound } from '../components/NotFound' +import appCss from '../styles/app.css?url' +import { seo } from '../utils/seo' +import { DescopeLogo } from '../components/DescopeLogo' +import { DescopeProvider } from '../integrations/descope/provider' +import { getSession } from '../integrations/descope/server' + +export const Route = createRootRoute({ + beforeLoad: async () => { + const user = await getSession() + + return { + user, + } + }, + head: () => ({ + meta: [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + ...seo({ + title: 'TanStack Start + Descope', + description: `Authenticate users with Descope in a TanStack Start application.`, + }), + ], + links: [ + { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, + { + rel: 'preconnect', + href: 'https://fonts.gstatic.com', + crossOrigin: 'anonymous', + }, + { + rel: 'stylesheet', + href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap', + }, + { rel: 'stylesheet', href: appCss }, + ], + }), + errorComponent: (props) => { + return ( + + + + ) + }, + notFoundComponent: () => , + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + ) +} + +function RootDocument({ children }: { children: React.ReactNode }) { + const { user } = Route.useRouteContext() + + return ( + + + + + + +
+ {/* brand gradient accent */} +
+ +
+
{children}
+ +
+ + + + ) +} diff --git a/examples/react/start-descope/src/routes/_authed.tsx b/examples/react/start-descope/src/routes/_authed.tsx new file mode 100644 index 0000000000..d388da427e --- /dev/null +++ b/examples/react/start-descope/src/routes/_authed.tsx @@ -0,0 +1,11 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/_authed')({ + beforeLoad: ({ context }) => { + // The root loader populates `context.user` from the validated session. + // Redirect unauthenticated visitors to the login flow. + if (!context.user) { + throw redirect({ to: '/login' }) + } + }, +}) diff --git a/examples/react/start-descope/src/routes/_authed/profile.tsx b/examples/react/start-descope/src/routes/_authed/profile.tsx new file mode 100644 index 0000000000..ee13071813 --- /dev/null +++ b/examples/react/start-descope/src/routes/_authed/profile.tsx @@ -0,0 +1,84 @@ +import { ClientOnly, createFileRoute, useRouter } from '@tanstack/react-router' +import { UserProfile, useUser } from '@descope/react-sdk' + +export const Route = createFileRoute('/_authed/profile')({ + component: ProfileComp, +}) + +function ProfileComp() { + const router = useRouter() + // `context.user` comes from the server-validated session (SSR-safe). + const { user: sessionUser } = Route.useRouteContext() + // `useUser()` fetches the full user profile on the client. + const { user, isUserLoading } = useUser() + + return ( +
+
+

+ Profile +

+

+ Manage your account with the Descope user profile widget, or inspect + the raw session below. +

+
+ + {/* + * The `UserProfile` widget is a self-service account page (name, avatar, + * passkeys, MFA, ...) rendered by Descope. Every project ships a default + * widget with the id `user-profile-widget`. Like the login flow it is a + * web component, so it only renders on the client. + */} +
+ }> + router.navigate({ to: '/logout' })} + /> + +
+ +
+
+ + Server session{' '} + + — validated during SSR + + +
+            {JSON.stringify(sessionUser, null, 2)}
+          
+
+ +
+ + Client user{' '} + + — from useUser() + + + {isUserLoading ? ( +

Loading…

+ ) : ( +
+              {JSON.stringify(user, null, 2)}
+            
+ )} +
+
+
+ ) +} + +function WidgetSkeleton() { + return ( +
+
+
+
+
+ ) +} diff --git a/examples/react/start-descope/src/routes/index.tsx b/examples/react/start-descope/src/routes/index.tsx new file mode 100644 index 0000000000..5e7081a6b0 --- /dev/null +++ b/examples/react/start-descope/src/routes/index.tsx @@ -0,0 +1,236 @@ +import { Link, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +const FEATURES = [ + { + title: 'Session validation', + description: + 'Descope sessions validated on the server on every request, during SSR and client navigation.', + href: 'https://docs.descope.com/session-management/session-validation', + icon: , + }, + { + title: 'React SDK', + description: + 'Protected routes, hooks, and auth state from @descope/react-sdk.', + href: 'https://docs.descope.com/getting-started/react', + icon: , + }, + { + title: 'Flows & widgets', + description: + 'The hosted sign-up-or-in flow and the self-service user profile widget.', + href: 'https://docs.descope.com/widgets', + icon: , + }, +] + +function Home() { + const { user } = Route.useRouteContext() + + return ( +
+ {/* background glow */} +
+ +
+ {/* left column */} +
+ + + Example integration + + +

+ TanStack Start + Descope +

+ +

+ Server-validated sessions, protected routes, and Descope's + hosted login flow & user profile widget — in one small example. +

+ +
+ {user ? ( + + View your profile + + + ) : ( + + Sign in to get started + + + )} + + + View docs + +
+ + {user ? ( +

+ Signed in as{' '} + + {user.email ?? user.userId} + +

+ ) : ( +

+ New to Descope?{' '} + + Create a free account → + +

+ )} +
+ + {/* right column — feature cards, each linking to its docs */} + +
+
+ ) +} + +function ArrowRightIcon() { + return ( + + + + ) +} + +function BookIcon() { + return ( + + + + + ) +} + +function ServerIcon() { + return ( + + + + + + ) +} + +function RoutesIcon() { + return ( + + + + + + ) +} + +function WidgetsIcon() { + return ( + + + + + + + ) +} diff --git a/examples/react/start-descope/src/routes/login.tsx b/examples/react/start-descope/src/routes/login.tsx new file mode 100644 index 0000000000..8980663057 --- /dev/null +++ b/examples/react/start-descope/src/routes/login.tsx @@ -0,0 +1,54 @@ +import { + ClientOnly, + createFileRoute, + redirect, + useRouter, +} from '@tanstack/react-router' +import { Descope } from '@descope/react-sdk' + +export const Route = createFileRoute('/login')({ + beforeLoad: ({ context }) => { + // Already signed in — no need to show the login flow. + if (context.user) { + throw redirect({ to: '/' }) + } + }, + component: LoginComp, +}) + +function LoginComp() { + const router = useRouter() + + return ( +
+
+

+ Sign in +

+ {/* + * The Descope flow renders as a web component that requires the + * browser, so `ClientOnly` keeps it out of the SSR pass. + */} + Loading…

+ } + > + { + // Re-run the root loader so the server picks up the new session + // cookie, then send the user home. + await router.invalidate() + await router.navigate({ to: '/' }) + }} + onError={(err) => { + console.error('Descope flow error', err) + }} + /> +
+
+
+ ) +} diff --git a/examples/react/start-descope/src/routes/logout.tsx b/examples/react/start-descope/src/routes/logout.tsx new file mode 100644 index 0000000000..f80949bd62 --- /dev/null +++ b/examples/react/start-descope/src/routes/logout.tsx @@ -0,0 +1,37 @@ +import { createFileRoute, useRouter } from '@tanstack/react-router' +import { useDescope } from '@descope/react-sdk' +import * as React from 'react' +import { clearServerSession } from '../integrations/descope/server' + +export const Route = createFileRoute('/logout')({ + preload: false, + component: LogoutComp, +}) + +function LogoutComp() { + const sdk = useDescope() + const router = useRouter() + + React.useEffect(() => { + async function doLogout() { + try { + // Revoke the session with Descope and clear the client-side tokens. + await sdk.logout() + } catch (err) { + console.error('Descope logout error', err) + } + // Clear the server-readable cookies, then refresh and go home. + await clearServerSession() + await router.invalidate() + await router.navigate({ to: '/' }) + } + doLogout() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( +
+

Signing out…

+
+ ) +} diff --git a/examples/react/start-descope/src/start.ts b/examples/react/start-descope/src/start.ts new file mode 100644 index 0000000000..ffcaf6dceb --- /dev/null +++ b/examples/react/start-descope/src/start.ts @@ -0,0 +1,22 @@ +import { createCsrfMiddleware, createStart } from '@tanstack/react-start' +import { descopeMiddleware } from './integrations/descope/middleware' + +/** + * Protect server functions (same-origin RPC endpoints) from cross-site + * requests — without this, another site could make a logged-in visitor's + * browser call them with the Descope session cookies attached. + */ +const csrfMiddleware = createCsrfMiddleware({ + filter: (ctx) => ctx.handlerType === 'serverFn', +}) + +/** + * Configure TanStack Start with the Descope request middleware, which validates + * (and refreshes) the session on every server request and exposes the user via + * the global start context. + */ +export const startInstance = createStart(() => { + return { + requestMiddleware: [csrfMiddleware, descopeMiddleware()], + } +}) diff --git a/examples/react/start-descope/src/styles/app.css b/examples/react/start-descope/src/styles/app.css new file mode 100644 index 0000000000..aa327f784d --- /dev/null +++ b/examples/react/start-descope/src/styles/app.css @@ -0,0 +1,36 @@ +@import 'tailwindcss' source('../'); + +@theme { + --font-sans: + 'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', + 'Segoe UI Emoji'; +} + +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-800, currentcolor); + } +} + +@layer base { + html { + color-scheme: dark; + } + + * { + @apply border-gray-800; + } + + html, + body { + @apply bg-[#0a0f1a] text-gray-200; + } + + .using-mouse * { + outline: none !important; + } +} diff --git a/examples/react/start-descope/src/utils/seo.ts b/examples/react/start-descope/src/utils/seo.ts new file mode 100644 index 0000000000..d18ad84b74 --- /dev/null +++ b/examples/react/start-descope/src/utils/seo.ts @@ -0,0 +1,33 @@ +export const seo = ({ + title, + description, + keywords, + image, +}: { + title: string + description?: string + image?: string + keywords?: string +}) => { + const tags = [ + { title }, + { name: 'description', content: description }, + { name: 'keywords', content: keywords }, + { name: 'twitter:title', content: title }, + { name: 'twitter:description', content: description }, + { name: 'twitter:creator', content: '@tannerlinsley' }, + { name: 'twitter:site', content: '@tannerlinsley' }, + { name: 'og:type', content: 'website' }, + { name: 'og:title', content: title }, + { name: 'og:description', content: description }, + ...(image + ? [ + { name: 'twitter:image', content: image }, + { name: 'twitter:card', content: 'summary_large_image' }, + { name: 'og:image', content: image }, + ] + : []), + ] + + return tags +} diff --git a/examples/react/start-descope/tsconfig.json b/examples/react/start-descope/tsconfig.json new file mode 100644 index 0000000000..cef9369516 --- /dev/null +++ b/examples/react/start-descope/tsconfig.json @@ -0,0 +1,21 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "paths": { + "~/*": ["./src/*"] + }, + "noEmit": true + } +} diff --git a/examples/react/start-descope/vite.config.ts b/examples/react/start-descope/vite.config.ts new file mode 100644 index 0000000000..ed6799764a --- /dev/null +++ b/examples/react/start-descope/vite.config.ts @@ -0,0 +1,14 @@ +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import { defineConfig } from 'vite' +import viteReact from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + server: { + port: 3000, + }, + resolve: { + tsconfigPaths: true, + }, + plugins: [tailwindcss(), tanstackStart(), viteReact()], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96847b4044..ccfbcc321f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9601,6 +9601,52 @@ importers: specifier: ^8.0.14 version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) + examples/react/start-descope: + dependencies: + '@descope/node-sdk': + specifier: ^2.12.0 + version: 2.12.0 + '@descope/react-sdk': + specifier: ^2.30.3 + version: 2.30.3(@reduxjs/toolkit@2.12.0(react@19.2.3))(@types/react@19.2.9)(immer@11.1.11)(react@19.2.3)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1) + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/react-router-devtools': + specifier: workspace:^ + version: link:../../../packages/react-router-devtools + '@tanstack/react-start': + specifier: workspace:* + version: link:../../../packages/react-start + react: + specifier: ^19.2.3 + version: 19.2.3 + react-dom: + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) + devDependencies: + '@tailwindcss/vite': + specifier: ^4.2.2 + version: 4.2.2(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + '@types/react': + specifier: ^19.2.8 + version: 19.2.9 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.9) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + tailwindcss: + specifier: ^4.2.2 + version: 4.2.2 + typescript: + specifier: ^6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.14 + version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) + examples/react/start-i18n-paraglide: dependencies: '@tanstack/react-devtools': @@ -14509,6 +14555,66 @@ packages: resolution: {integrity: sha512-Y6+WUMsTFWE5jb20IFP4YGa5IrGY/+a/FbOSjDF/wz9gepU2hwCYSXRHP/vPwBvwcY3SVMASt4yXxbXNXigmZQ==} engines: {node: '>=18'} + '@descope/access-key-management-widget@0.9.5': + resolution: {integrity: sha512-0Nv9BkWAxNPoy97ZeV9VNZDxI94DDZGw6lV4PUH9u8Gr4qkFXumtx69tAqDk85hh0sTNwx3ovGbkuD6/SrlMCg==} + + '@descope/applications-portal-widget@0.8.5': + resolution: {integrity: sha512-BBYrgro80dAY4mXJfLasgO6rTqlROio6/Eu+RXCLQGLCFfSRXUkYMhUMJGJw7kI0GFXzI/eM+E5yNpooS91QTQ==} + + '@descope/audit-management-widget@0.8.5': + resolution: {integrity: sha512-Ge6ax8RKn7HQklGXwpA7mmLStlukRxxdfxTbvxMr7OSSmdIN3dUHIxR0jyuPP1DOB4+gCBjCqu0MWvZIXVZvNw==} + + '@descope/core-js-sdk@2.66.0': + resolution: {integrity: sha512-/c/khU5dJSLYFYgqnSiyY5qnIgM+eKCBcOGhAV3c8EGY9dBRfrynh8aFGQrLlXtUm/tDqeW7NCRVSCheXbHRag==} + + '@descope/escape-markdown@0.1.6': + resolution: {integrity: sha512-3ENrye8fyyAp88w8OoKGsEl/kH0GgcrPiDRoOajhCODOJnoJ8P/4/giBEjWZV/FKllW4Z5mSXGACEmlINOZLCA==} + + '@descope/node-sdk@2.12.0': + resolution: {integrity: sha512-5TUfyLEUn7nCvXiMFaHyevW5447lDRzrZZf7qzcwEFRxLaMKU3e0Dx+fmGuMGt0bv0b+HAJbxaWBgzZHx0P4mQ==} + engines: {node: '>= 16.0.0'} + + '@descope/outbound-applications-widget@0.5.2': + resolution: {integrity: sha512-tArpts6/8NOMno3MLR6fXUVGDkoNn5nD5eNEby3V44P1zqN0hD3UX2mQNyApvqQtPNAxzReFo83izsnEroNz/A==} + + '@descope/react-sdk@2.30.3': + resolution: {integrity: sha512-/sPBGDtSLhHSCy6ZNeyTarddp4usPknBAWcDVaWIUVU5XFyWRPPuzYDfM1g7LUr4Fx3kSGj3T34wAeV46QkJ6Q==} + peerDependencies: + '@types/react': ^19.2.8 + react: ^19.2.3 + + '@descope/role-management-widget@0.9.5': + resolution: {integrity: sha512-/lZR3DHB7pbTsn9k4Nfi0unDZzn9lCkanIKIyKh4zsixtZIpB9Ya8NvYpr5q4RRxI2jKi9VwE0ymqfWVVgjrnA==} + + '@descope/sdk-component-drivers@0.14.0': + resolution: {integrity: sha512-keIenQU3kUuhCJN3eNgv2234fPnzWHjIaKeSY7ecpA/iAQkAu+u9RU4lpF2FzJAG8uWzel2tjZl194kLruq2kQ==} + + '@descope/sdk-helpers@0.9.0': + resolution: {integrity: sha512-+hjxU4EDfBDSrO/YfxRbFKczbygIFJb7/gf3NfHAnOv0JTqPAxmzQYdXd53S12v1NS80kak/soLibRWfKDiG1Q==} + + '@descope/sdk-mixins@0.23.0': + resolution: {integrity: sha512-yj1pvnGg8Qn8xPrihN0PLd9AJ0uhrtN1QrQt+PMjQR6s5akR5U7HgMetxZYnb56cdfgMUcL9UQwcAwTbtcLRIQ==} + peerDependencies: + '@reduxjs/toolkit': ^2.0.1 + immer: ^10.0.3 + redux: 5.0.1 + redux-thunk: 3.1.0 + + '@descope/tenant-profile-widget@0.8.2': + resolution: {integrity: sha512-i465o+6vUlE/lNdJre69cUUqBnf7+cGxDiq4Y9aKbI93iAv2CQHoyfuqroG+H5EESlFslrTy+nKuMfThYzAI2g==} + + '@descope/user-management-widget@0.15.2': + resolution: {integrity: sha512-GPTR9e74ooihA2qit92gsbFroLDFZYBydm0RER6MF+Nt8uPcl0v7sQKCnSCwQkYY48xmP02VKzcR+3GZhGlZkw==} + + '@descope/user-profile-widget@0.14.1': + resolution: {integrity: sha512-mkqchsLWVyVdTeHfpAvs4YN4k3+flcydMp1xl2E5WEAq9RK3q+nWSRkOwZfw+TJikW5YENwUT0TT1O1ysupi7Q==} + + '@descope/web-component@3.70.0': + resolution: {integrity: sha512-cHG3+/407HFM9Idrd6XEf/sW+3opxcBVvdmtvI75HOn56fIwWiVzQvBBO1beFepDnaIqM2a2RWnPU3wwIuIwwA==} + + '@descope/web-js-sdk@1.51.1': + resolution: {integrity: sha512-qFWm57WGDbl6TqV/K5W3GG42X4x3EorHy3xzb63T//zbY0xcAEtwCV13TP+giPpVsDgcGYIBpt7h+S6OIAEeOg==} + '@discoveryjs/json-ext@0.5.7': resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} @@ -15502,6 +15608,9 @@ packages: '@fastify/proxy-addr@5.1.0': resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@fingerprintjs/fingerprintjs-pro@3.11.6': + resolution: {integrity: sha512-ohN4lorSzV0fzs8fELWuO3bvMMfzF5qyQSOWBvqwGaq6yPP+XSRJOeFdFFiwAqFWYJDEoDOlQluGfa0Rfk7mAQ==} + '@firebase/analytics-compat@0.2.18': resolution: {integrity: sha512-Hw9mzsSMZaQu6wrTbi3kYYwGw9nBqOHr47pVLxfr5v8CalsdrG5gfs9XUlPOZjHRVISp3oQrh1j7d3E+ulHPjQ==} peerDependencies: @@ -17954,6 +18063,17 @@ packages: '@types/react-dom': optional: true + '@reduxjs/toolkit@2.12.0': + resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==} + peerDependencies: + react: ^19.2.3 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@rolldown/binding-android-arm64@1.0.0-rc.9': resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18963,6 +19083,9 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@stylistic/eslint-plugin@5.10.0': resolution: {integrity: sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -21264,6 +21387,9 @@ packages: engines: {node: '>=20'} hasBin: true + cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -22829,6 +22955,9 @@ packages: immer@10.1.1: resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + immer@11.1.11: + resolution: {integrity: sha512-qzXuyXAkPySAGYkfsAwodDPWT8Zm7/Uo5BNt4BjhMhG5WlWyZZ4wQqnWwdS8kjlQ1Cwu6gjw3A6+0gTQwlyYtw==} + immutable@5.1.4: resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} @@ -23154,6 +23283,9 @@ packages: jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + jose@5.2.2: + resolution: {integrity: sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg==} + jose@6.1.0: resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==} @@ -23175,6 +23307,9 @@ packages: resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} engines: {node: '>=14'} + js-cookie@3.0.8: + resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==} + js-image-generator@1.0.4: resolution: {integrity: sha512-ckb7kyVojGAnArouVR+5lBIuwU1fcrn7E/YYSd0FK7oIngAkMmRvHASLro9Zt5SQdWToaI66NybG+OGxPw/HlQ==} @@ -23347,6 +23482,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libphonenumber-js@1.11.17: + resolution: {integrity: sha512-Jr6v8thd5qRlOlc6CslSTzGzzQW03uiscab7KHQZX1Dfo4R6n6FDhZ0Hri6/X7edLIDv9gl4VMZXhxTjLnl0VQ==} + libsql@0.3.19: resolution: {integrity: sha512-Aj5cQ5uk/6fHdmeW0TiXK42FqUlwx7ytmMLPSaUQPin5HKKKuUPD62MAbN4OEweGBBI7q1BekoEN4gPUEL6MZA==} cpu: [x64, arm64, wasm32] @@ -24851,6 +24989,14 @@ packages: reduce-configs@1.1.1: resolution: {integrity: sha512-EYtsVGAQarE8daT54cnaY1PIknF2VB78ug6Zre2rs36EsJfC40EG6hmTU2A2P1ZuXnKAt2KI0fzOGHcX7wzdPw==} + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -24908,6 +25054,12 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + + reselect@5.2.0: + resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} + resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -28135,6 +28287,247 @@ snapshots: gonzales-pe: 4.3.0 node-source-walk: 7.0.1 + '@descope/access-key-management-widget@0.9.5(react@19.2.3)': + dependencies: + '@descope/sdk-component-drivers': 0.14.0 + '@descope/sdk-helpers': 0.9.0 + '@descope/sdk-mixins': 0.23.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@10.1.1)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1) + '@descope/web-js-sdk': 1.51.1 + '@reduxjs/toolkit': 2.12.0(react@19.2.3) + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + tslib: 2.8.1 + transitivePeerDependencies: + - react + - react-redux + + '@descope/applications-portal-widget@0.8.5(react@19.2.3)': + dependencies: + '@descope/sdk-component-drivers': 0.14.0 + '@descope/sdk-helpers': 0.9.0 + '@descope/sdk-mixins': 0.23.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@10.1.1)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1) + '@descope/web-js-sdk': 1.51.1 + '@reduxjs/toolkit': 2.12.0(react@19.2.3) + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + tslib: 2.8.1 + transitivePeerDependencies: + - react + - react-redux + + '@descope/audit-management-widget@0.8.5(react@19.2.3)': + dependencies: + '@descope/sdk-component-drivers': 0.14.0 + '@descope/sdk-helpers': 0.9.0 + '@descope/sdk-mixins': 0.23.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@10.1.1)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1) + '@descope/web-js-sdk': 1.51.1 + '@reduxjs/toolkit': 2.12.0(react@19.2.3) + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + tslib: 2.8.1 + transitivePeerDependencies: + - react + - react-redux + + '@descope/core-js-sdk@2.66.0': + dependencies: + jwt-decode: 4.0.0 + tslib: 2.8.1 + + '@descope/escape-markdown@0.1.6': {} + + '@descope/node-sdk@2.12.0': + dependencies: + '@descope/core-js-sdk': 2.66.0 + cross-fetch: 4.1.0 + jose: 5.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - encoding + + '@descope/outbound-applications-widget@0.5.2(react@19.2.3)': + dependencies: + '@descope/sdk-component-drivers': 0.14.0 + '@descope/sdk-helpers': 0.9.0 + '@descope/sdk-mixins': 0.23.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@10.1.1)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1) + '@descope/web-component': 3.70.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@10.1.1)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1) + '@descope/web-js-sdk': 1.51.1 + '@reduxjs/toolkit': 2.12.0(react@19.2.3) + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + tslib: 2.8.1 + transitivePeerDependencies: + - react + - react-redux + + '@descope/react-sdk@2.30.3(@reduxjs/toolkit@2.12.0(react@19.2.3))(@types/react@19.2.9)(immer@11.1.11)(react@19.2.3)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1)': + dependencies: + '@descope/access-key-management-widget': 0.9.5(react@19.2.3) + '@descope/applications-portal-widget': 0.8.5(react@19.2.3) + '@descope/audit-management-widget': 0.8.5(react@19.2.3) + '@descope/core-js-sdk': 2.66.0 + '@descope/outbound-applications-widget': 0.5.2(react@19.2.3) + '@descope/role-management-widget': 0.9.5(react@19.2.3) + '@descope/sdk-helpers': 0.9.0 + '@descope/tenant-profile-widget': 0.8.2(react@19.2.3) + '@descope/user-management-widget': 0.15.2(react@19.2.3) + '@descope/user-profile-widget': 0.14.1(react@19.2.3) + '@descope/web-component': 3.70.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@11.1.11)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1) + '@descope/web-js-sdk': 1.51.1 + '@types/react': 19.2.9 + react: 19.2.3 + transitivePeerDependencies: + - '@reduxjs/toolkit' + - immer + - react-redux + - redux + - redux-thunk + + '@descope/role-management-widget@0.9.5(react@19.2.3)': + dependencies: + '@descope/sdk-component-drivers': 0.14.0 + '@descope/sdk-helpers': 0.9.0 + '@descope/sdk-mixins': 0.23.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@10.1.1)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1) + '@descope/web-js-sdk': 1.51.1 + '@reduxjs/toolkit': 2.12.0(react@19.2.3) + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + tslib: 2.8.1 + transitivePeerDependencies: + - react + - react-redux + + '@descope/sdk-component-drivers@0.14.0': + dependencies: + '@descope/sdk-helpers': 0.9.0 + tslib: 2.8.1 + + '@descope/sdk-helpers@0.9.0': + dependencies: + tslib: 2.8.1 + + '@descope/sdk-mixins@0.23.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@10.1.1)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1)': + dependencies: + '@descope/sdk-component-drivers': 0.14.0 + '@descope/sdk-helpers': 0.9.0 + '@reduxjs/toolkit': 2.12.0(react@19.2.3) + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + tslib: 2.8.1 + + '@descope/sdk-mixins@0.23.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@11.1.11)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1)': + dependencies: + '@descope/sdk-component-drivers': 0.14.0 + '@descope/sdk-helpers': 0.9.0 + '@reduxjs/toolkit': 2.12.0(react@19.2.3) + immer: 11.1.11 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + tslib: 2.8.1 + + '@descope/tenant-profile-widget@0.8.2(react@19.2.3)': + dependencies: + '@descope/sdk-component-drivers': 0.14.0 + '@descope/sdk-helpers': 0.9.0 + '@descope/sdk-mixins': 0.23.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@10.1.1)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1) + '@descope/web-component': 3.70.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@10.1.1)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1) + '@descope/web-js-sdk': 1.51.1 + '@reduxjs/toolkit': 2.12.0(react@19.2.3) + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + tslib: 2.8.1 + optionalDependencies: + '@descope/core-js-sdk': 2.66.0 + transitivePeerDependencies: + - react + - react-redux + + '@descope/user-management-widget@0.15.2(react@19.2.3)': + dependencies: + '@descope/sdk-component-drivers': 0.14.0 + '@descope/sdk-helpers': 0.9.0 + '@descope/sdk-mixins': 0.23.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@10.1.1)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1) + '@descope/web-component': 3.70.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@10.1.1)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1) + '@descope/web-js-sdk': 1.51.1 + '@reduxjs/toolkit': 2.12.0(react@19.2.3) + immer: 10.1.1 + libphonenumber-js: 1.11.17 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + tslib: 2.8.1 + transitivePeerDependencies: + - react + - react-redux + + '@descope/user-profile-widget@0.14.1(react@19.2.3)': + dependencies: + '@descope/sdk-component-drivers': 0.14.0 + '@descope/sdk-helpers': 0.9.0 + '@descope/sdk-mixins': 0.23.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@10.1.1)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1) + '@descope/web-component': 3.70.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@10.1.1)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1) + '@descope/web-js-sdk': 1.51.1 + '@reduxjs/toolkit': 2.12.0(react@19.2.3) + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + tslib: 2.8.1 + optionalDependencies: + '@descope/core-js-sdk': 2.66.0 + transitivePeerDependencies: + - react + - react-redux + + '@descope/web-component@3.70.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@10.1.1)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1)': + dependencies: + '@descope/escape-markdown': 0.1.6 + '@descope/sdk-helpers': 0.9.0 + '@descope/sdk-mixins': 0.23.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@10.1.1)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1) + '@descope/web-js-sdk': 1.51.1 + '@fingerprintjs/fingerprintjs-pro': 3.11.6 + tslib: 2.8.1 + transitivePeerDependencies: + - '@reduxjs/toolkit' + - immer + - redux + - redux-thunk + + '@descope/web-component@3.70.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@11.1.11)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1)': + dependencies: + '@descope/escape-markdown': 0.1.6 + '@descope/sdk-helpers': 0.9.0 + '@descope/sdk-mixins': 0.23.0(@reduxjs/toolkit@2.12.0(react@19.2.3))(immer@11.1.11)(redux-thunk@3.1.0(redux@5.0.1))(redux@5.0.1) + '@descope/web-js-sdk': 1.51.1 + '@fingerprintjs/fingerprintjs-pro': 3.11.6 + tslib: 2.8.1 + transitivePeerDependencies: + - '@reduxjs/toolkit' + - immer + - redux + - redux-thunk + + '@descope/web-js-sdk@1.51.1': + dependencies: + '@descope/core-js-sdk': 2.66.0 + '@fingerprintjs/fingerprintjs-pro': 3.11.6 + js-cookie: 3.0.8 + jwt-decode: 4.0.0 + tslib: 2.8.1 + '@discoveryjs/json-ext@0.5.7': {} '@electric-sql/pglite-socket@0.0.6(@electric-sql/pglite@0.3.2)': @@ -28833,6 +29226,10 @@ snapshots: '@fastify/forwarded': 3.0.1 ipaddr.js: 2.2.0 + '@fingerprintjs/fingerprintjs-pro@3.11.6': + dependencies: + tslib: 2.8.1 + '@firebase/analytics-compat@0.2.18(@firebase/app-compat@0.2.51)(@firebase/app@0.11.2)': dependencies: '@firebase/analytics': 0.10.12(@firebase/app@0.11.2) @@ -31498,6 +31895,17 @@ snapshots: '@types/react': 19.2.9 '@types/react-dom': 19.2.3(@types/react@19.2.9) + '@reduxjs/toolkit@2.12.0(react@19.2.3)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.11 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.2.0 + optionalDependencies: + react: 19.2.3 + '@rolldown/binding-android-arm64@1.0.0-rc.9': optional: true @@ -32326,6 +32734,8 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@standard-schema/utils@0.3.0': {} + '@stylistic/eslint-plugin@5.10.0(eslint@9.22.0(jiti@2.7.0))': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.22.0(jiti@2.7.0)) @@ -35321,6 +35731,12 @@ snapshots: '@epic-web/invariant': 1.0.0 cross-spawn: 7.0.6 + cross-fetch@4.1.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -37182,6 +37598,8 @@ snapshots: immer@10.1.1: {} + immer@11.1.11: {} + immutable@5.1.4: {} import-fresh@3.3.0: @@ -37488,6 +37906,8 @@ snapshots: jju@1.4.0: {} + jose@5.2.2: {} + jose@6.1.0: {} jose@6.1.3: {} @@ -37506,6 +37926,8 @@ snapshots: js-cookie@3.0.5: {} + js-cookie@3.0.8: {} + js-image-generator@1.0.4: dependencies: jpeg-js: 0.4.4 @@ -37715,6 +38137,8 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + libphonenumber-js@1.11.17: {} + libsql@0.3.19: dependencies: '@neon-rs/load': 0.0.4 @@ -39625,6 +40049,12 @@ snapshots: reduce-configs@1.1.1: {} + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + reflect-metadata@0.2.2: {} regenerator-runtime@0.14.1: {} @@ -39679,6 +40109,10 @@ snapshots: requires-port@1.0.0: {} + reselect@5.1.1: {} + + reselect@5.2.0: {} + resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index de56e54dd7..384689c20b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -12,6 +12,7 @@ trustPolicyExclude: - '@netlify/edge-bundler@14.7.0' # existing Netlify dev transitive flagged by pnpm 11.9 trust downgrade checks - '@netlify/serverless-functions-api@2.7.1' # existing Netlify dev transitive flagged by pnpm 11.9 trust downgrade checks - '@netlify/zip-it-and-ship-it@14.1.11' # existing Netlify dev transitive flagged by pnpm 11.9 trust downgrade checks + - 'reselect@5.1.1' # transitive of @descope/react-sdk widgets (start-descope example), provenance dropped in this release packages: - 'packages/*' From 11566151039d17acc75dd1e3e3e5ce893adfa643 Mon Sep 17 00:00:00 2001 From: Maria Mantsurova Date: Wed, 8 Jul 2026 14:48:51 +0300 Subject: [PATCH 2/3] feat: add loading state for user profile and login components --- .../src/routes/_authed/profile.tsx | 18 +++++-- .../react/start-descope/src/routes/login.tsx | 54 ++++++++++++------- 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/examples/react/start-descope/src/routes/_authed/profile.tsx b/examples/react/start-descope/src/routes/_authed/profile.tsx index ee13071813..0a19e594a5 100644 --- a/examples/react/start-descope/src/routes/_authed/profile.tsx +++ b/examples/react/start-descope/src/routes/_authed/profile.tsx @@ -1,4 +1,5 @@ import { ClientOnly, createFileRoute, useRouter } from '@tanstack/react-router' +import { useState } from 'react' import { UserProfile, useUser } from '@descope/react-sdk' export const Route = createFileRoute('/_authed/profile')({ @@ -11,6 +12,9 @@ function ProfileComp() { const { user: sessionUser } = Route.useRouteContext() // `useUser()` fetches the full user profile on the client. const { user, isUserLoading } = useUser() + // The widget's web component loads asynchronously; `onReady` fires once it + // has rendered, so we show a skeleton until then. + const [widgetReady, setWidgetReady] = useState(false) return (
@@ -32,11 +36,15 @@ function ProfileComp() { */}
}> - router.navigate({ to: '/logout' })} - /> + {!widgetReady && } +
+ setWidgetReady(true)} + onLogout={() => router.navigate({ to: '/logout' })} + /> +
diff --git a/examples/react/start-descope/src/routes/login.tsx b/examples/react/start-descope/src/routes/login.tsx index 8980663057..cf4167e01b 100644 --- a/examples/react/start-descope/src/routes/login.tsx +++ b/examples/react/start-descope/src/routes/login.tsx @@ -4,6 +4,7 @@ import { redirect, useRouter, } from '@tanstack/react-router' +import { useState } from 'react' import { Descope } from '@descope/react-sdk' export const Route = createFileRoute('/login')({ @@ -18,6 +19,10 @@ export const Route = createFileRoute('/login')({ function LoginComp() { const router = useRouter() + // The flow's web component has to download and fetch its flow definition + // before it renders anything. `onReady` fires once that's done — until then + // we show a loader so the user never sees an empty box. + const [ready, setReady] = useState(false) return (
@@ -27,28 +32,39 @@ function LoginComp() { {/* * The Descope flow renders as a web component that requires the - * browser, so `ClientOnly` keeps it out of the SSR pass. + * browser, so `ClientOnly` keeps it out of the SSR pass. The loader + * covers two phases: SSR + hydration (via the `fallback`), and the + * web component's own load (until `onReady`). We keep the flow mounted + * but hidden during that second phase so it can actually load. */} - Loading…

- } - > - { - // Re-run the root loader so the server picks up the new session - // cookie, then send the user home. - await router.invalidate() - await router.navigate({ to: '/' }) - }} - onError={(err) => { - console.error('Descope flow error', err) - }} - /> + }> + {!ready && } +
+ setReady(true)} + onSuccess={async () => { + // Re-run the root loader so the server picks up the new session + // cookie, then send the user home. + await router.invalidate() + await router.navigate({ to: '/' }) + }} + onError={(err) => { + console.error('Descope flow error', err) + }} + /> +
) } + +function Loader() { + return ( +
+
+
+ ) +} From d3fb2c0130faf043ba7c0b43a9f97f92387f8df7 Mon Sep 17 00:00:00 2001 From: Maria Mantsurova Date: Thu, 9 Jul 2026 11:33:54 +0300 Subject: [PATCH 3/3] feedback improvments --- examples/react/start-descope/.gitignore | 3 ++- .../src/integrations/descope/session.server.ts | 1 + examples/react/start-descope/src/routes/logout.tsx | 13 +++++++++---- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/examples/react/start-descope/.gitignore b/examples/react/start-descope/.gitignore index cd1bb463bd..3d09de62a7 100644 --- a/examples/react/start-descope/.gitignore +++ b/examples/react/start-descope/.gitignore @@ -12,7 +12,8 @@ yarn.lock /build/ /api/ /server/build -/public/build# Sentry Config File +/public/build +# Sentry Config File .env.sentry-build-plugin /test-results/ /playwright-report/ diff --git a/examples/react/start-descope/src/integrations/descope/session.server.ts b/examples/react/start-descope/src/integrations/descope/session.server.ts index ea06db91f3..27525c608e 100644 --- a/examples/react/start-descope/src/integrations/descope/session.server.ts +++ b/examples/react/start-descope/src/integrations/descope/session.server.ts @@ -50,6 +50,7 @@ export async function resolveSessionFromRequest(): Promise { setCookie(SESSION_COOKIE, authInfo.jwt, { httpOnly: false, sameSite: 'lax', + secure: true, path: '/', }) } diff --git a/examples/react/start-descope/src/routes/logout.tsx b/examples/react/start-descope/src/routes/logout.tsx index f80949bd62..886ee3f7ca 100644 --- a/examples/react/start-descope/src/routes/logout.tsx +++ b/examples/react/start-descope/src/routes/logout.tsx @@ -20,10 +20,15 @@ function LogoutComp() { } catch (err) { console.error('Descope logout error', err) } - // Clear the server-readable cookies, then refresh and go home. - await clearServerSession() - await router.invalidate() - await router.navigate({ to: '/' }) + try { + // Clear the server-readable cookies, then refresh and go home. + await clearServerSession() + await router.invalidate() + await router.navigate({ to: '/' }) + } catch (err) { + console.error('Logout cleanup error', err) + await router.navigate({ to: '/' }) + } } doLogout() // eslint-disable-next-line react-hooks/exhaustive-deps