diff --git a/src/keycloak-theme/KcApp.tsx b/src/keycloak-theme/KcApp.tsx index 1235b6b..81d83ca 100644 --- a/src/keycloak-theme/KcApp.tsx +++ b/src/keycloak-theme/KcApp.tsx @@ -2,31 +2,29 @@ import "./KcApp.css"; import { lazy, Suspense } from "react"; import type { KcContext } from "./kcContext"; import { useI18n } from "./i18n"; -import Fallback, { defaultKcProps, type KcProps, type PageProps } from "keycloakify"; -import Template from "./Template"; -import DefaultTemplate from "keycloakify/lib/Template"; +import Fallback, { type PageProps } from "keycloakify"; + +const Template = lazy(() => import("./Template")); +const DefaultTemplate = lazy(() => import("keycloakify/Template")); // You can uncomment this to see the values passed by the main app before redirecting. //import { foo, bar } from "./valuesTransferredOverUrl"; //console.log(`Values passed by the main app in the URL parameter:`, { foo, bar }); -const Login = lazy(()=> import("./pages/Login")); +const Login = lazy(() => import("./pages/Login")); // If you can, favor register-user-profile.ftl over register.ftl, see: https://docs.keycloakify.dev/realtime-input-validation const Register = lazy(() => import("./pages/Register")); const RegisterUserProfile = lazy(() => import("./pages/RegisterUserProfile")); const Terms = lazy(() => import("./pages/Terms")); const MyExtraPage1 = lazy(() => import("./pages/MyExtraPage1")); const MyExtraPage2 = lazy(() => import("./pages/MyExtraPage2")); -const Info = lazy(()=> import("keycloakify/lib/pages/Info")); +const Info = lazy(() => import("keycloakify/pages/Info")); -// This is like editing the theme.properties +// This is like adding classes to theme.properties // https://github.com/keycloak/keycloak/blob/11.0.3/themes/src/main/resources/theme/keycloak/login/theme.properties -const kcProps: KcProps = { - ...defaultKcProps, +const classes: PageProps["classes"] = { // NOTE: The classes are defined in ./KcApp.css - // You can add your classes alongside thoses that are present in the default Keycloak theme... - "kcHtmlClass": [...defaultKcProps.kcHtmlClass, "my-root-class"], - // ...or overwrite + "kcHtmlClass": "my-root-class", "kcHeaderWrapperClass": "my-color my-font" }; @@ -40,36 +38,26 @@ export default function App(props: { kcContext: KcContext; }) { //NOTE: Locales not yet downloaded, we could as well display a loading progress but it's usually a matter of milliseconds. return null; } - + /* * Examples assuming i18n.currentLanguageTag === "en": * i18n.msg("access-denied") === Access denied * i18n.msg("foo") === foo in English */ - const pageProps: Omit, "kcContext"> = { - i18n, - // Here we have overloaded the default template, however you could use the default one with: - //Template: DefaultTemplate, - Template, - // Wether or not we should download the CSS and JS resources that comes with the default Keycloak theme. - doFetchDefaultThemeResources: true, - ...kcProps, - }; - return ( {(() => { switch (kcContext.pageId) { - case "login.ftl": return ; - case "register.ftl": return ; - case "register-user-profile.ftl": return - case "terms.ftl": return ; - case "my-extra-page-1.ftl": return ; - case "my-extra-page-2.ftl": return ; + case "login.ftl": return ; + case "register.ftl": return ; + case "register-user-profile.ftl": return + case "terms.ftl": return ; + case "my-extra-page-1.ftl": return ; + case "my-extra-page-2.ftl": return ; // We choose to use the default Template for the Info page and to download the theme resources. - case "info.ftl": return ; - default: return ; + case "info.ftl": return ; + default: return ; } })()} diff --git a/src/keycloak-theme/Template.tsx b/src/keycloak-theme/Template.tsx index bd4e46a..57a1566 100644 --- a/src/keycloak-theme/Template.tsx +++ b/src/keycloak-theme/Template.tsx @@ -1,13 +1,9 @@ -// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/components/shared/Template.tsx - -// You can replace all relative imports by cherry picking files from the keycloakify module. -// For example, the following import: -// import { assert } from "./tools/assert"; -// becomes: -import { assert } from "keycloakify/lib/tools/assert"; -import { clsx } from "keycloakify/lib/tools/clsx"; -import type { TemplateProps } from "keycloakify/lib/KcProps"; -import { usePrepareTemplate } from "keycloakify/lib/Template"; +// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/Template.tsx +import { assert } from "keycloakify/tools/assert"; +import { clsx } from "keycloakify/tools/clsx"; +import { usePrepareTemplate } from "keycloakify/lib/usePrepareTemplate"; +import { type TemplateProps, defaultTemplateClasses } from "keycloakify/TemplateProps"; +import { useGetClassName } from "keycloakify/lib/useGetClassName"; import type { KcContext } from "./kcContext"; import type { I18n } from "./i18n"; @@ -24,24 +20,29 @@ export default function Template(props: TemplateProps) { infoNode = null, kcContext, i18n, - doFetchDefaultThemeResources, - stylesCommon, - styles, - scripts, - kcHtmlClass + doUseDefaultCss, + classes } = props; + const { getClassName } = useGetClassName({ + "defaultClasses": !doUseDefaultCss ? undefined : defaultTemplateClasses, + classes + }); + const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n; const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext; const { isReady } = usePrepareTemplate({ - doFetchDefaultThemeResources, - stylesCommon, - styles, - scripts, + "doFetchDefaultThemeResources": doUseDefaultCss, url, - kcHtmlClass + "stylesCommon": [ + "node_modules/patternfly/dist/css/patternfly.min.css", + "node_modules/patternfly/dist/css/patternfly-additions.min.css", + "lib/zocial/zocial.css" + ], + "styles": ["css/login.css"], + "htmlClassName": getClassName("kcHtmlClass") }); if (!isReady) { @@ -49,18 +50,18 @@ export default function Template(props: TemplateProps) { } return ( -
-
-
+
+
+
{msg("loginTitleHtml", realm.displayNameHtml)}
-
-
+
+
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
-
+
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} @@ -82,8 +83,8 @@ export default function Template(props: TemplateProps) { )} {!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? ( displayRequiredFields ? ( -
-
+
+
* {msg("requiredFields")} @@ -97,20 +98,20 @@ export default function Template(props: TemplateProps) {

{headerNode}

) ) : displayRequiredFields ? ( -
-
+
+
* {msg("requiredFields")}
{showUsernameNode} -
+
- + {msg("restartLoginTooltip")}
@@ -121,12 +122,12 @@ export default function Template(props: TemplateProps) { ) : ( <> {showUsernameNode} -
+
- + {msg("restartLoginTooltip")}
@@ -140,10 +141,10 @@ export default function Template(props: TemplateProps) { {/* App-initiated actions should not see warning messages about the need to complete the action during login. */} {displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && (
- {message.type === "success" && } - {message.type === "warning" && } - {message.type === "error" && } - {message.type === "info" && } + {message.type === "success" && } + {message.type === "warning" && } + {message.type === "error" && } + {message.type === "info" && } ) { id="kc-select-try-another-way-form" action={url.loginAction} method="post" - className={clsx(displayWide && props.kcContentWrapperClass)} + className={clsx(displayWide && getClassName("kcContentWrapperClass"))} > -
-
+
+ @@ -175,8 +184,8 @@ export default function Template(props: TemplateProps) { )} {displayInfo && ( -
-
+
+
{infoNode}
diff --git a/src/keycloak-theme/i18n.ts b/src/keycloak-theme/i18n.ts index 3b89a1e..bbeab0b 100644 --- a/src/keycloak-theme/i18n.ts +++ b/src/keycloak-theme/i18n.ts @@ -1,32 +1,24 @@ -import { useI18n as useI18nBase } from "keycloakify"; +import { createUseI18n } from "keycloakify"; -type Props = Omit[0], "extraMessages">; - -export function useI18n(props: Props) { - const { kcContext } = props; - return useI18nBase({ - kcContext, - // NOTE: Here you can override the default i18n messages - // or define new ones that, for example, you would have - // defined in the Keycloak admin UI for UserProfile - // https://user-images.githubusercontent.com/6702424/182050652-522b6fe6-8ee5-49df-aca3-dba2d33f24a5.png - "extraMessages": { - "en": { - "alphanumericalCharsOnly": "Only alphanumerical characters", - "gender": "Gender", - // Here we overwrite the default english value for the message "doForgotPassword" - // that is "Forgot Password?" see: https://github.com/InseeFrLab/keycloakify/blob/f0ae5ea908e0aa42391af323b6d5e2fd371af851/src/lib/i18n/generated_messages/18.0.1/login/en.ts#L17 - "doForgotPassword": "I forgot my password", - }, - "fr": { - /* spell-checker: disable */ - "alphanumericalCharsOnly": "Caractère alphanumérique uniquement", - "gender": "Genre", - "doForgotPassword": "J'ai oublié mon mot de passe" - /* spell-checker: enable */ - }, - }, - }); -} +export const { useI18n } = createUseI18n({ + // NOTE: Here you can override the default i18n messages + // or define new ones that, for example, you would have + // defined in the Keycloak admin UI for UserProfile + // https://user-images.githubusercontent.com/6702424/182050652-522b6fe6-8ee5-49df-aca3-dba2d33f24a5.png + en: { + alphanumericalCharsOnly: "Only alphanumerical characters", + gender: "Gender", + // Here we overwrite the default english value for the message "doForgotPassword" + // that is "Forgot Password?" see: https://github.com/InseeFrLab/keycloakify/blob/f0ae5ea908e0aa42391af323b6d5e2fd371af851/src/lib/i18n/generated_messages/18.0.1/login/en.ts#L17 + doForgotPassword: "I forgot my password", + }, + fr: { + /* spell-checker: disable */ + alphanumericalCharsOnly: "Caractère alphanumérique uniquement", + gender: "Genre", + doForgotPassword: "J'ai oublié mon mot de passe" + /* spell-checker: enable */ + } +}); export type I18n = NonNullable>; diff --git a/src/keycloak-theme/kcContext.ts b/src/keycloak-theme/kcContext.ts index 078efeb..44b8069 100644 --- a/src/keycloak-theme/kcContext.ts +++ b/src/keycloak-theme/kcContext.ts @@ -1,4 +1,4 @@ -import { getKcContext } from "keycloakify/lib/getKcContext"; +import { getKcContext } from "keycloakify"; export type KcContextExtension = // NOTE: A 'keycloakify' field must be added diff --git a/src/keycloak-theme/pages/Login.tsx b/src/keycloak-theme/pages/Login.tsx index 1c1559a..932f1ae 100644 --- a/src/keycloak-theme/pages/Login.tsx +++ b/src/keycloak-theme/pages/Login.tsx @@ -1,198 +1,204 @@ +// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/pages/Login.tsx + import { useState, type FormEventHandler } from "react"; -// This is a copy paste from https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/Login.tsx -// You can replace all relative imports by cherry picking files from the keycloakify module. -// For example, the following import: -// import { clsx } from "./tools/clsx"; -// becomes: -import { clsx } from "keycloakify/lib/tools/clsx"; -import { useConstCallback } from "keycloakify/lib/tools/useConstCallback"; -import type { PageProps } from "keycloakify/lib/KcProps"; -// Here use your own KcContext and I18n that you might have overloaded. +import { clsx } from "keycloakify/tools/clsx"; +import { useConstCallback } from "keycloakify/tools/useConstCallback"; +import { type PageProps, defaultClasses } from "keycloakify/pages/PageProps"; +import { useGetClassName } from "keycloakify/lib/useGetClassName"; import type { KcContext } from "../kcContext"; import type { I18n } from "../i18n"; -export default function Login(props: PageProps, I18n>) { - const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props; +export default function Login(props: PageProps, I18n>) { + const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; - const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext; + const { getClassName } = useGetClassName({ + "defaultClasses": !doUseDefaultCss ? undefined : defaultClasses, + classes + }); - const { msg, msgStr } = i18n; + const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext; - const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false); + const { msg, msgStr } = i18n; - const onSubmit = useConstCallback>(e => { - e.preventDefault(); + const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false); - setIsLoginButtonDisabled(true); + const onSubmit = useConstCallback>(e => { + e.preventDefault(); - const formElement = e.target as HTMLFormElement; + setIsLoginButtonDisabled(true); - //NOTE: Even if we login with email Keycloak expect username and password in - //the POST request. - formElement.querySelector("input[name='email']")?.setAttribute("name", "username"); + const formElement = e.target as HTMLFormElement; - formElement.submit(); - }); + //NOTE: Even if we login with email Keycloak expect username and password in + //the POST request. + formElement.querySelector("input[name='email']")?.setAttribute("name", "username"); - return ( -