From d9a65fba15315ca7adbb9f85c0cdb6fa122be641 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Thu, 13 Nov 2025 10:09:26 -0800 Subject: [PATCH 1/2] Use sync authToken in AuthKit React --- react/src/lib/auth-context.ts | 10 ++++ react/src/lib/auth-provider.tsx | 22 ++++++++ react/src/lib/use-auth.ts | 28 ++++++++++ react/src/root.tsx | 8 ++- react/src/routes/_dashboard.tsx | 2 +- react/src/routes/_dashboard.users.tsx | 74 +++++++++++++++------------ 6 files changed, 104 insertions(+), 40 deletions(-) create mode 100644 react/src/lib/auth-context.ts create mode 100644 react/src/lib/auth-provider.tsx create mode 100644 react/src/lib/use-auth.ts diff --git a/react/src/lib/auth-context.ts b/react/src/lib/auth-context.ts new file mode 100644 index 0000000..865e149 --- /dev/null +++ b/react/src/lib/auth-context.ts @@ -0,0 +1,10 @@ +import * as React from "react"; + +interface AuthContextType { + authToken: string | null; +} + +const AuthContext = React.createContext(null); +AuthContext.displayName = "AuthContext"; + +export { AuthContext, type AuthContextType }; diff --git a/react/src/lib/auth-provider.tsx b/react/src/lib/auth-provider.tsx new file mode 100644 index 0000000..9fd99bf --- /dev/null +++ b/react/src/lib/auth-provider.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; +import { AuthKitProvider } from "@workos-inc/authkit-react"; +import { AuthContext } from "./auth-context"; + +export const AuthProvider: React.FC< + React.ComponentProps +> = ({ children, ...props }) => { + const [authToken, setAuthToken] = React.useState(null); + return ( + { + props.onRefresh?.(payload); + setAuthToken(payload.accessToken); + }} + > + + {children} + + + ); +}; diff --git a/react/src/lib/use-auth.ts b/react/src/lib/use-auth.ts new file mode 100644 index 0000000..6c7a64c --- /dev/null +++ b/react/src/lib/use-auth.ts @@ -0,0 +1,28 @@ +import * as React from "react"; +import { useAuth as useAuthKit } from "@workos-inc/authkit-react"; +import { AuthContext, type AuthContextType } from "./auth-context"; + +type UseAuthReturnType = Omit, "isLoading"> & + ( + | { isLoading: true; authToken: null } + | { isLoading: false; authToken: string } + ); + +export function useAuth(): UseAuthReturnType { + const authKit = useAuthKit(); + const context = React.useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthProvider"); + } + + if (authKit.isLoading || !context.authToken) { + return { ...authKit, ...context, isLoading: true, authToken: null }; + } + + return { + ...authKit, + ...context, + isLoading: false, + authToken: context.authToken, + }; +} diff --git a/react/src/root.tsx b/react/src/root.tsx index e4b59d8..16c0eab 100644 --- a/react/src/root.tsx +++ b/react/src/root.tsx @@ -1,8 +1,6 @@ import { Flex, Heading, Link, Text, Theme } from "@radix-ui/themes"; import { ErrorBoundary as ReactErrorBoundary } from "react-error-boundary"; import { - Links, - Meta, Outlet, Link as RouterLink, ScrollRestoration, @@ -12,7 +10,7 @@ import { import * as React from "react"; import { preconnect } from "react-dom"; import { getApiProps, getDevtools } from "~/lib/env"; -import { AuthKitProvider } from "@workos-inc/authkit-react"; +import { AuthProvider } from "~/lib/auth-provider"; import { WorkOsWidgets } from "@workos-inc/widgets"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; @@ -40,13 +38,13 @@ function Providers({ children }: { children: React.ReactNode }) { )} > - + {children} - + ); } diff --git a/react/src/routes/_dashboard.tsx b/react/src/routes/_dashboard.tsx index 8d26a98..70a2f93 100644 --- a/react/src/routes/_dashboard.tsx +++ b/react/src/routes/_dashboard.tsx @@ -11,7 +11,7 @@ import { Text, VisuallyHidden, } from "@radix-ui/themes"; -import { useAuth } from "@workos-inc/authkit-react"; +import { useAuth } from "src/lib/use-auth"; import { Outlet, useNavigation } from "react-router"; import { PrimaryNav, PrimaryNavItem } from "src/ui/dashboard"; import { SuperAppMark } from "src/ui/svgs"; diff --git a/react/src/routes/_dashboard.users.tsx b/react/src/routes/_dashboard.users.tsx index b0ca459..5612aaf 100644 --- a/react/src/routes/_dashboard.users.tsx +++ b/react/src/routes/_dashboard.users.tsx @@ -1,16 +1,19 @@ -import { UsersManagement } from "@workos-inc/widgets"; -import { Flex, Heading, Separator, Text } from "@radix-ui/themes"; +import { UsersManagement, UsersManagementLoading } from "@workos-inc/widgets"; +import { Button, Flex, Heading, Separator, Text } from "@radix-ui/themes"; import { Navigate } from "react-router"; -import { useAuth } from "@workos-inc/authkit-react"; +import { useAuth } from "src/lib/use-auth"; export default function Users() { const auth = useAuth(); if (auth.isLoading) { return ( - - Authenticating... - + <> + SuperApp | Users + + + + ); } @@ -22,20 +25,11 @@ export default function Users() { return ( <> SuperApp | No organization - - -
- - User does not belong to an organization - - - - -
-
-
+ + + ); } @@ -43,20 +37,32 @@ export default function Users() { return ( <> SuperApp | Users - - -
- - Users - - Manage and invite users for the SuperApp team -
- -
- -
-
-
+ + + ); } + +function Layout({ + children, + subtitle, +}: { + children: React.ReactNode; + subtitle: string; +}) { + return ( + + +
+ + Users + + {subtitle} +
+ +
{children}
+
+
+ ); +} From eac419e4a68bacd427a810df38e8c4d39a1ee1a6 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Thu, 13 Nov 2025 13:41:29 -0800 Subject: [PATCH 2/2] tweaks --- react/src/lib/use-auth.ts | 71 ++++++++++++++++++++++++--- react/src/routes/_auth.signin.tsx | 2 +- react/src/routes/_dashboard.tsx | 6 +-- react/src/routes/_dashboard.users.tsx | 14 ++++-- 4 files changed, 76 insertions(+), 17 deletions(-) diff --git a/react/src/lib/use-auth.ts b/react/src/lib/use-auth.ts index 6c7a64c..ae888ee 100644 --- a/react/src/lib/use-auth.ts +++ b/react/src/lib/use-auth.ts @@ -1,12 +1,11 @@ import * as React from "react"; import { useAuth as useAuthKit } from "@workos-inc/authkit-react"; -import { AuthContext, type AuthContextType } from "./auth-context"; +import { AuthContext } from "./auth-context"; -type UseAuthReturnType = Omit, "isLoading"> & - ( - | { isLoading: true; authToken: null } - | { isLoading: false; authToken: string } - ); +type AuthKit = ReturnType; +type User = Exclude; + +type UseAuthReturnType = Omit & State; export function useAuth(): UseAuthReturnType { const authKit = useAuthKit(); @@ -15,14 +14,70 @@ export function useAuth(): UseAuthReturnType { throw new Error("useAuth must be used within an AuthProvider"); } - if (authKit.isLoading || !context.authToken) { - return { ...authKit, ...context, isLoading: true, authToken: null }; + if (authKit.isLoading) { + return { + ...authKit, + ...context, + status: "loading", + isLoading: true, + user: null, + authToken: null, + }; + } + + if (!authKit.user) { + return { + ...authKit, + ...context, + status: "unauthenticated", + isLoading: false, + user: null, + authToken: null, + }; } return { ...authKit, ...context, + status: "authenticated", isLoading: false, + user: authKit.user, authToken: context.authToken, }; } + +type Status = + | "loading" + | "authenticated" + | "authenticated-with-token" + | "unauthenticated"; + +interface SharedState { + status: Status; + isLoading: boolean; + user: User | null; + authToken: string | null; +} + +interface LoadingState extends SharedState { + status: "loading"; + isLoading: true; + user: null; + authToken: null; +} + +interface AuthenticatedState extends SharedState { + status: "authenticated"; + isLoading: false; + user: User; + authToken: string | null; +} + +interface UnauthenticatedState extends SharedState { + status: "unauthenticated"; + isLoading: false; + user: null; + authToken: null; +} + +export type State = LoadingState | AuthenticatedState | UnauthenticatedState; diff --git a/react/src/routes/_auth.signin.tsx b/react/src/routes/_auth.signin.tsx index 623e8d4..a6ccf16 100644 --- a/react/src/routes/_auth.signin.tsx +++ b/react/src/routes/_auth.signin.tsx @@ -1,6 +1,6 @@ import { Button, Flex, Heading } from "@radix-ui/themes"; import { Link } from "react-router"; -import { useAuth } from "@workos-inc/authkit-react"; +import { useAuth } from "~/lib/use-auth"; export default function HomePage() { const auth = useAuth(); diff --git a/react/src/routes/_dashboard.tsx b/react/src/routes/_dashboard.tsx index 70a2f93..0528f12 100644 --- a/react/src/routes/_dashboard.tsx +++ b/react/src/routes/_dashboard.tsx @@ -11,10 +11,10 @@ import { Text, VisuallyHidden, } from "@radix-ui/themes"; -import { useAuth } from "src/lib/use-auth"; +import { useAuth } from "~/lib/use-auth"; import { Outlet, useNavigation } from "react-router"; -import { PrimaryNav, PrimaryNavItem } from "src/ui/dashboard"; -import { SuperAppMark } from "src/ui/svgs"; +import { PrimaryNav, PrimaryNavItem } from "~/ui/dashboard"; +import { SuperAppMark } from "~/ui/svgs"; export default function DashboardLayout() { const navigation = useNavigation(); diff --git a/react/src/routes/_dashboard.users.tsx b/react/src/routes/_dashboard.users.tsx index 5612aaf..cbfdc12 100644 --- a/react/src/routes/_dashboard.users.tsx +++ b/react/src/routes/_dashboard.users.tsx @@ -1,7 +1,7 @@ import { UsersManagement, UsersManagementLoading } from "@workos-inc/widgets"; import { Button, Flex, Heading, Separator, Text } from "@radix-ui/themes"; import { Navigate } from "react-router"; -import { useAuth } from "src/lib/use-auth"; +import { useAuth } from "~/lib/use-auth"; export default function Users() { const auth = useAuth(); @@ -10,7 +10,7 @@ export default function Users() { return ( <> SuperApp | Users - + @@ -25,7 +25,7 @@ export default function Users() { return ( <> SuperApp | No organization - + @@ -37,8 +37,12 @@ export default function Users() { return ( <> SuperApp | Users - - + + {auth.authToken ? ( + + ) : ( + + )} );