Skip to content

Commit b489ac5

Browse files
authored
Merge pull request #158 from kinde-oss/feat/protected-route-component
feat: ProtectedRoute component
2 parents 72269d5 + 39a78e9 commit b489ac5

File tree

3 files changed

+143
-0
lines changed

3 files changed

+143
-0
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
"types": "./dist/src/utils/index.d.ts",
2222
"import": "./dist/utils.mjs",
2323
"require": "./dist/utils.cjs"
24+
},
25+
"./react-router": {
26+
"types": "./dist/src/components/react-router-dom/index.d.ts",
27+
"import": "./dist/react-router.mjs",
28+
"require": "./dist/react-router.cjs"
2429
}
2530
},
2631
"scripts": {
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { useState, useEffect } from "react";
2+
import { useKindeAuth } from "../../hooks/useKindeAuth";
3+
import { has } from "@kinde/js-utils";
4+
import React from "react";
5+
6+
interface ProtectedRouteProps {
7+
children: React.ReactNode;
8+
has?: Parameters<typeof has>[0];
9+
fallbackPath?: string;
10+
}
11+
12+
// Fallback component when react-router-dom is not available
13+
function ProtectedRouteFallback({ children }: { children: React.ReactNode }) {
14+
return (
15+
<div
16+
style={{
17+
padding: "20px",
18+
textAlign: "center",
19+
color: "#666",
20+
fontFamily: "system-ui, -apple-system, sans-serif",
21+
}}
22+
>
23+
<h3>Protected Route</h3>
24+
<p>This component requires react-router-dom to be installed.</p>
25+
<p>
26+
Please install it: <code>npm install react-router-dom</code>
27+
</p>
28+
<div
29+
style={{
30+
marginTop: "20px",
31+
padding: "10px",
32+
border: "1px solid #ddd",
33+
borderRadius: "4px",
34+
}}
35+
>
36+
{children}
37+
</div>
38+
</div>
39+
);
40+
}
41+
42+
export default function ProtectedRoute({
43+
children,
44+
has: hasParams,
45+
fallbackPath = "/",
46+
}: ProtectedRouteProps) {
47+
const { isLoading, isAuthenticated } = useKindeAuth();
48+
const [accessLoading, setAccessLoading] = useState(false);
49+
const [hasAccess, setHasAccess] = useState<boolean | null>(null);
50+
const [NavigateComponent, setNavigateComponent] =
51+
useState<React.ComponentType<{ to: string; replace?: boolean }> | null>(
52+
null,
53+
);
54+
const [isRouterAvailable, setIsRouterAvailable] = useState<boolean | null>(
55+
null,
56+
);
57+
58+
// Dynamically import react-router-dom
59+
useEffect(() => {
60+
const loadRouter = async () => {
61+
try {
62+
// @ts-expect-error - react-router-dom is an optional dependency
63+
const { Navigate } = await import("react-router-dom");
64+
setNavigateComponent(() => Navigate);
65+
setIsRouterAvailable(true);
66+
} catch {
67+
console.warn(
68+
"react-router-dom is not available. ProtectedRoute will render a fallback.",
69+
);
70+
setIsRouterAvailable(false);
71+
}
72+
};
73+
74+
loadRouter();
75+
}, []);
76+
77+
useEffect(() => {
78+
const checkAccess = async () => {
79+
if (!hasParams) {
80+
setHasAccess(true);
81+
return;
82+
}
83+
84+
setAccessLoading(true);
85+
try {
86+
const result = await has(hasParams);
87+
setHasAccess(result);
88+
} catch (error) {
89+
console.error("Access check failed:", error);
90+
setHasAccess(false);
91+
} finally {
92+
setAccessLoading(false);
93+
}
94+
};
95+
96+
if (isAuthenticated) {
97+
checkAccess();
98+
}
99+
}, [hasParams, isAuthenticated]);
100+
101+
// Show loading while checking router availability or auth/access
102+
if (isRouterAvailable === null || isLoading || accessLoading) {
103+
return <div>Loading...</div>;
104+
}
105+
106+
// Show loading while checking router availability
107+
if (isRouterAvailable === null) {
108+
return <div>Loading...</div>;
109+
}
110+
111+
// If react-router-dom is not available, show fallback
112+
if (isRouterAvailable === false) {
113+
return <ProtectedRouteFallback>{children}</ProtectedRouteFallback>;
114+
}
115+
116+
if (!isAuthenticated) {
117+
return NavigateComponent ? (
118+
<NavigateComponent to={fallbackPath} replace />
119+
) : (
120+
<div>Redirecting...</div>
121+
);
122+
}
123+
124+
if (hasAccess === false) {
125+
return NavigateComponent ? (
126+
<NavigateComponent to={fallbackPath} replace />
127+
) : (
128+
<div>Redirecting...</div>
129+
);
130+
}
131+
132+
if (hasAccess === null) {
133+
return <div>Loading...</div>;
134+
}
135+
136+
return <>{children}</>;
137+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as ProtectedRoute } from "./ProtectedRoute";

0 commit comments

Comments
 (0)