Skip to content

Commit 22ffc00

Browse files
ksroda-saclaude
andcommitted
Address Copilot review: add loading state, error test, refresh test
- Add auth.isLoading handling to token-refresh TokenStatus - Add error rendering test and refresh button click test - Fix README to accurately describe both auto and manual refresh - Regenerate snippets Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0f6584b commit 22ffc00

4 files changed

Lines changed: 45 additions & 7 deletions

File tree

samples/react/token-refresh/README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# React SPA — Token Refresh
22

3-
Minimal React app demonstrating token refresh using `react-oidc-context` with the `offline_access` scope. The library automatically exchanges refresh tokens for new access tokens before they expire.
3+
Minimal React app demonstrating token refresh using `react-oidc-context` with the `offline_access` scope. Tokens are refreshed automatically before they expire, and can also be refreshed manually via a button.
4+
5+
## Prerequisites
6+
7+
- `offline_access` scope enabled on the client
8+
- `refresh_token` grant type enabled in workspace OAuth settings and in the application's Grant Types
49

510
## Setup
611

@@ -13,4 +18,6 @@ Minimal React app demonstrating token refresh using `react-oidc-context` with th
1318

1419
- OIDC configuration with `offline_access` scope for refresh tokens
1520
- Automatic token refresh via refresh tokens (no iframe needed)
16-
- Displaying token expiry and refresh status
21+
- Manual token refresh via `signinSilent()` button
22+
- Displaying token expiry information
23+
- Error handling with OAuth error hints

samples/react/token-refresh/src/App.test.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { render, screen } from "@testing-library/react";
2-
import { describe, expect, it, vi } from "vitest";
1+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
2+
import { afterEach, describe, expect, it, vi } from "vitest";
33

44
const mockUseAuth = vi.fn();
55
const mockSigninSilent = vi.fn();
@@ -14,6 +14,7 @@ vi.mock("react-oidc-context", () => ({
1414
import App from "./App";
1515

1616
describe("App (Token Refresh)", () => {
17+
afterEach(() => cleanup());
1718
it("renders sign in button when not authenticated", () => {
1819
mockUseAuth.mockReturnValue({
1920
isAuthenticated: false,
@@ -37,4 +38,30 @@ describe("App (Token Refresh)", () => {
3738
expect(screen.getByText("Welcome, Jane")).toBeInTheDocument();
3839
expect(screen.getByText("Refresh token now")).toBeInTheDocument();
3940
});
41+
42+
it("renders error message when authentication fails", () => {
43+
mockUseAuth.mockReturnValue({
44+
isAuthenticated: false,
45+
isLoading: false,
46+
error: new Error("Unable to refresh token"),
47+
});
48+
render(<App />);
49+
expect(screen.getByText(/Unable to refresh token/)).toBeInTheDocument();
50+
expect(screen.getByText("Try again")).toBeInTheDocument();
51+
});
52+
53+
it("calls signinSilent when refresh button is clicked", () => {
54+
mockUseAuth.mockReturnValue({
55+
isAuthenticated: true,
56+
isLoading: false,
57+
signinSilent: mockSigninSilent,
58+
user: {
59+
profile: { given_name: "Jane" },
60+
expires_at: Math.floor(Date.now() / 1000) + 3600,
61+
},
62+
});
63+
render(<App />);
64+
fireEvent.click(screen.getByText("Refresh token now"));
65+
expect(mockSigninSilent).toHaveBeenCalled();
66+
});
4067
});

samples/react/token-refresh/src/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ const oidcConfig = {
1818
function TokenStatus() {
1919
const auth = useAuth();
2020

21+
if (auth.isLoading) {
22+
return <p>Loading...</p>;
23+
}
24+
2125
if (auth.error) {
2226
const hint = new URLSearchParams(window.location.search).get("error_hint");
2327
return (

snippets.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,18 @@
6565
{
6666
"step": 3,
6767
"description": "Display token status and trigger manual refresh",
68-
"code": "function TokenStatus() {\n const auth = useAuth();\n\n if (auth.error) {\n const hint = new URLSearchParams(window.location.search).get(\"error_hint\");\n return (\n <div style={{ color: \"red\" }}>\n <p>Error: {auth.error.message}</p>\n {hint && <p>{hint}</p>}\n <button onClick={() => auth.signinRedirect()}>Try again</button>\n </div>\n );\n }\n\n if (!auth.isAuthenticated) {\n return <button onClick={() => auth.signinRedirect()}>Sign in</button>;\n }\n\n const expiresAt = auth.user?.expires_at\n ? new Date(auth.user.expires_at * 1000).toLocaleTimeString()\n : \"unknown\";\n\n const handleRefresh = async () => {\n try {\n await auth.signinSilent();\n } catch (error) {\n console.error(\"Token refresh failed:\", error);\n }\n };\n\n return (\n <div>\n <p>Welcome, {auth.user?.profile.given_name}</p>\n <p>Token expires at: {expiresAt}</p>\n <button onClick={handleRefresh}>Refresh token now</button>\n </div>\n );\n}",
68+
"code": "function TokenStatus() {\n const auth = useAuth();\n\n if (auth.isLoading) {\n return <p>Loading...</p>;\n }\n\n if (auth.error) {\n const hint = new URLSearchParams(window.location.search).get(\"error_hint\");\n return (\n <div style={{ color: \"red\" }}>\n <p>Error: {auth.error.message}</p>\n {hint && <p>{hint}</p>}\n <button onClick={() => auth.signinRedirect()}>Try again</button>\n </div>\n );\n }\n\n if (!auth.isAuthenticated) {\n return <button onClick={() => auth.signinRedirect()}>Sign in</button>;\n }\n\n const expiresAt = auth.user?.expires_at\n ? new Date(auth.user.expires_at * 1000).toLocaleTimeString()\n : \"unknown\";\n\n const handleRefresh = async () => {\n try {\n await auth.signinSilent();\n } catch (error) {\n console.error(\"Token refresh failed:\", error);\n }\n };\n\n return (\n <div>\n <p>Welcome, {auth.user?.profile.given_name}</p>\n <p>Token expires at: {expiresAt}</p>\n <button onClick={handleRefresh}>Refresh token now</button>\n </div>\n );\n}",
6969
"file": "src/App.tsx",
7070
"lang": "tsx",
71-
"lines": "16-55"
71+
"lines": "16-59"
7272
},
7373
{
7474
"step": 4,
7575
"description": "Wrap your app with AuthProvider",
7676
"code": "export default function App() {\n return (\n <AuthProvider {...oidcConfig}>\n <h1>SecureAuth Token Refresh Demo</h1>\n <TokenStatus />\n </AuthProvider>\n );\n}",
7777
"file": "src/App.tsx",
7878
"lang": "tsx",
79-
"lines": "58-67"
79+
"lines": "62-71"
8080
}
8181
],
8282
"framework": "react",

0 commit comments

Comments
 (0)