Skip to content

Commit 9209333

Browse files
damienlethiecclaude
andcommitted
feat: add themed LoginPageExpired page
Add DSFR-styled LoginPageExpired page (login-page-expired.ftl) that displays: - Two action options in DSFR callouts with grid layout - pageExpiredMsg1: Restart authentication flow (secondary button with refresh icon) - pageExpiredMsg2: Continue where you left off (primary button with arrow icon) - Fully DSFR-adapted with native components (fr-callout, fr-btn, fr-grid) - No custom Alert component, uses Keycloak default messages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent db9a070 commit 9209333

File tree

3 files changed

+91
-0
lines changed

3 files changed

+91
-0
lines changed

src/login/KcPage.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const Login = lazy(() => import("./pages/Login"));
1313
const Register = lazy(() => import("./pages/Register"));
1414
const LoginUpdateProfile = lazy(() => import("./pages/LoginUpdateProfile"));
1515
const LoginUpdatePassword = lazy(() => import("./pages/LoginUpdatePassword"));
16+
const LoginPageExpired = lazy(() => import("./pages/LoginPageExpired"));
1617

1718
const doMakeUserConfirmPassword = false;
1819

@@ -77,6 +78,14 @@ export default function KcPage(props: { kcContext: KcContext }) {
7778
doUseDefaultCss={false}
7879
/>
7980
);
81+
case "login-page-expired.ftl":
82+
return (
83+
<LoginPageExpired
84+
{...{ kcContext, i18n, classes }}
85+
Template={Template}
86+
doUseDefaultCss={false}
87+
/>
88+
);
8089
default:
8190
return (
8291
<DefaultPage
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { createKcPageStory } from "../KcPageStory";
3+
4+
const { KcPageStory } = createKcPageStory({ pageId: "login-page-expired.ftl" });
5+
6+
const meta = {
7+
title: "login/login-page-expired.ftl",
8+
component: KcPageStory
9+
} satisfies Meta<typeof KcPageStory>;
10+
11+
export default meta;
12+
13+
type Story = StoryObj<typeof meta>;
14+
15+
export const Default: Story = {
16+
render: () => <KcPageStory />
17+
};
18+
19+
/**
20+
* WithCustomUrl:
21+
* - Purpose: Tests the page with a custom restart flow URL.
22+
* - Scenario: Simulates a page expired scenario with a specific URL to restart the authentication flow.
23+
* - Key Aspect: Ensures the "click here to login" link points to the correct restart URL.
24+
*/
25+
export const WithCustomUrl: Story = {
26+
render: () => (
27+
<KcPageStory
28+
kcContext={{
29+
url: {
30+
loginRestartFlowUrl: "https://hubee.com/auth/restart"
31+
}
32+
}}
33+
/>
34+
)
35+
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { PageProps } from "keycloakify/login/pages/PageProps";
2+
import type { KcContext } from "../KcContext";
3+
import type { I18n } from "../i18n";
4+
import { fr } from "@codegouvfr/react-dsfr";
5+
6+
export default function LoginPageExpired(props: PageProps<Extract<KcContext, { pageId: "login-page-expired.ftl" }>, I18n>) {
7+
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
8+
9+
const { url } = kcContext;
10+
11+
const { msg } = i18n;
12+
13+
return (
14+
<Template
15+
kcContext={kcContext}
16+
i18n={i18n}
17+
doUseDefaultCss={doUseDefaultCss}
18+
classes={classes}
19+
displayMessage={false}
20+
headerNode={msg("pageExpiredTitle")}
21+
>
22+
<div className={fr.cx("fr-grid-row", "fr-grid-row--gutters")}>
23+
<div className={fr.cx("fr-col-12")}>
24+
<div className={fr.cx("fr-callout")}>
25+
<p className={fr.cx("fr-callout__text")}>{msg("pageExpiredMsg1")}</p>
26+
<div style={{ display: "flex", justifyContent: "flex-end", marginTop: "1rem" }}>
27+
<a className={fr.cx("fr-btn", "fr-btn--secondary", "fr-btn--icon-right", "fr-icon-refresh-line")} href={url.loginRestartFlowUrl}>
28+
{msg("doClickHere")}
29+
</a>
30+
</div>
31+
</div>
32+
</div>
33+
34+
<div className={fr.cx("fr-col-12")}>
35+
<div className={fr.cx("fr-callout")}>
36+
<p className={fr.cx("fr-callout__text")}>{msg("pageExpiredMsg2")}</p>
37+
<div style={{ display: "flex", justifyContent: "flex-end", marginTop: "1rem" }}>
38+
<a className={fr.cx("fr-btn", "fr-btn--icon-right", "fr-icon-arrow-right-line")} href={url.loginAction}>
39+
{msg("doClickHere")}
40+
</a>
41+
</div>
42+
</div>
43+
</div>
44+
</div>
45+
</Template>
46+
);
47+
}

0 commit comments

Comments
 (0)