Skip to content

Commit 86eb47e

Browse files
RonenMarsclaude
andcommitted
feat(oauth): add error boundaries for OAuth authentication components
- Create comprehensive OAuthErrorBoundary component with fallback UI - Add proper TypeScript interfaces for error boundary props and state - Wrap Login page OAuth provider buttons with error boundary - Wrap AuthCallback component with error boundary for complete coverage - Implement graceful error handling with user-friendly error messages - Add retry functionality and navigation options in error states - Follow project conventions for logging, styling, and component structure 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 14afbd3 commit 86eb47e

File tree

6 files changed

+123
-7
lines changed

6 files changed

+123
-7
lines changed

src/components/atoms/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export { Link } from "@components/atoms/link";
1212
export { Loader } from "@components/atoms/loader";
1313
export { MermaidDiagram } from "@components/atoms/mermaidDiagram";
1414
export { LogoCatLarge } from "@components/atoms/logoCatLarge";
15+
export { OAuthErrorBoundary } from "@components/atoms/oauthErrorBoundary";
1516
export { PageTitle } from "@components/atoms/pageTitle";
1617
export { SearchInput } from "@components/atoms/searchInput";
1718
export { SecretInput } from "@components/atoms/secretInput";
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React, { Component } from "react";
2+
3+
import { useTranslation } from "react-i18next";
4+
import { Link } from "react-router-dom";
5+
6+
import { homepageURL } from "@constants/global.constants";
7+
import { OAuthErrorBoundaryProps, OAuthErrorBoundaryState } from "@interfaces/components";
8+
import { LoggerService } from "@services";
9+
10+
const OAuthErrorFallback = ({ error, resetError }: { error?: Error; resetError?: () => void }) => {
11+
const { t } = useTranslation("global", { keyPrefix: "oauthError" });
12+
13+
return (
14+
<div className="flex w-full flex-1 flex-col items-center justify-center py-5">
15+
<div className="mt-16 font-fira-code text-lg text-error">
16+
{t("authenticationError", "Authentication Error")}
17+
</div>
18+
<div className="mt-4 max-w-md text-center font-fira-code text-sm text-gray-600">
19+
{t("errorMessage", "An error occurred during authentication. Please try again.")}
20+
</div>
21+
{error?.message ? (
22+
<div className="mt-2 max-w-md text-center font-fira-code text-xs text-gray-500">{error.message}</div>
23+
) : null}
24+
<div className="mt-6 flex gap-4">
25+
{resetError ? (
26+
<button
27+
className="rounded bg-blue-500 px-4 py-2 font-fira-code text-sm text-white hover:opacity-80"
28+
onClick={resetError}
29+
type="button"
30+
>
31+
{t("tryAgain", "Try Again")}
32+
</button>
33+
) : null}
34+
<Link
35+
className="rounded border border-gray-300 px-4 py-2 font-fira-code text-sm text-gray-700 hover:bg-gray-1100"
36+
to={homepageURL}
37+
>
38+
{t("goHome", "Go Home")}
39+
</Link>
40+
</div>
41+
</div>
42+
);
43+
};
44+
45+
export class OAuthErrorBoundary extends Component<OAuthErrorBoundaryProps, OAuthErrorBoundaryState> {
46+
constructor(props: OAuthErrorBoundaryProps) {
47+
super(props);
48+
this.state = { hasError: false };
49+
}
50+
51+
static getDerivedStateFromError(error: Error): OAuthErrorBoundaryState {
52+
return { hasError: true, error };
53+
}
54+
55+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
56+
const { onError } = this.props;
57+
58+
LoggerService.error(
59+
"auth.oauth.boundary",
60+
`OAuth component error: ${error.message}, Stack: ${error.stack}, Component Stack: ${errorInfo.componentStack}`,
61+
true
62+
);
63+
64+
onError?.(error, errorInfo);
65+
}
66+
67+
resetErrorBoundary = () => {
68+
this.setState({ hasError: false, error: undefined });
69+
};
70+
71+
render() {
72+
const { hasError, error } = this.state;
73+
const { fallback, children } = this.props;
74+
75+
if (hasError) {
76+
if (fallback) {
77+
return fallback;
78+
}
79+
80+
return <OAuthErrorFallback error={error} resetError={this.resetErrorBoundary} />;
81+
}
82+
83+
return children;
84+
}
85+
}

src/components/pages/authCallback.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import { useLocation } from "react-router-dom";
66
import { isDevelopment } from "@constants/global.constants";
77
import { LoggerService } from "@services";
88

9+
import { OAuthErrorBoundary } from "@components/atoms";
10+
911
interface OAuthExchangeResult {
1012
ok: boolean;
1113
data?: unknown;
1214
error?: string;
1315
}
1416

15-
export const AuthCallback = () => {
17+
const AuthCallbackContent = () => {
1618
const location = useLocation();
1719
const sdk = useDescope();
1820
const [responseData, setResponseData] = useState<OAuthExchangeResult | null>(null);
@@ -188,3 +190,11 @@ export const AuthCallback = () => {
188190
</div>
189191
);
190192
};
193+
194+
export const AuthCallback = () => {
195+
return (
196+
<OAuthErrorBoundary>
197+
<AuthCallbackContent />
198+
</OAuthErrorBoundary>
199+
);
200+
};

src/components/pages/login.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { LoggerService } from "@services";
99
import { LoginPageProps } from "@src/interfaces/components";
1010
import { validateOAuthRedirectURL } from "@utilities/validateUrl.utils";
1111

12-
import { AHref, IconSvg, Loader } from "@components/atoms";
12+
import { AHref, IconSvg, Loader, OAuthErrorBoundary } from "@components/atoms";
1313
import { OAuthProviderButton } from "@components/molecules";
1414

1515
import { AKRoundLogo } from "@assets/image";
@@ -123,11 +123,18 @@ const Login = ({ handleSuccess, isLoggingIn }: LoginPageProps) => {
123123
{isLoggingIn ? (
124124
<Loader className="h-36" size="md" />
125125
) : (
126-
<div className="flex flex-col gap-3">
127-
{oauthProviders.map(({ id, label }) => (
128-
<OAuthProviderButton id={id} key={id} label={label} onClick={handleOAuthStart} />
129-
))}
130-
</div>
126+
<OAuthErrorBoundary>
127+
<div className="flex flex-col gap-3">
128+
{oauthProviders.map(({ id, label }) => (
129+
<OAuthProviderButton
130+
id={id}
131+
key={id}
132+
label={label}
133+
onClick={handleOAuthStart}
134+
/>
135+
))}
136+
</div>
137+
</OAuthErrorBoundary>
131138
)}
132139
</div>
133140
</div>

src/interfaces/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export type { ResizeButtonProps } from "./resizeButton.interface";
9191
export type { ChatbotToolbarProps } from "./chatbotToolbar.interface";
9292
export type { ChatbotLoadingStatesProps } from "./chatbotLoadingStates.interface";
9393
export type { CodeFixDiffEditorProps } from "./codeFixDiffEditor.interface";
94+
export type { OAuthErrorBoundaryProps, OAuthErrorBoundaryState } from "./oauthErrorBoundary.interface";
9495

9596
// Integration component interfaces
9697
export * from "./integrations";
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { ReactNode } from "react";
2+
3+
export interface OAuthErrorBoundaryProps {
4+
children: ReactNode;
5+
fallback?: ReactNode;
6+
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
7+
}
8+
9+
export interface OAuthErrorBoundaryState {
10+
hasError: boolean;
11+
error?: Error;
12+
}

0 commit comments

Comments
 (0)