update setup

This commit is contained in:
garronej 2023-03-19 15:50:39 +01:00
parent 39ae948f24
commit 93780a9702
10 changed files with 422 additions and 411 deletions

View file

@ -2,31 +2,29 @@ import "./KcApp.css";
import { lazy, Suspense } from "react"; import { lazy, Suspense } from "react";
import type { KcContext } from "./kcContext"; import type { KcContext } from "./kcContext";
import { useI18n } from "./i18n"; import { useI18n } from "./i18n";
import Fallback, { defaultKcProps, type KcProps, type PageProps } from "keycloakify"; import Fallback, { type PageProps } from "keycloakify";
import Template from "./Template";
import DefaultTemplate from "keycloakify/lib/Template"; 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. // You can uncomment this to see the values passed by the main app before redirecting.
//import { foo, bar } from "./valuesTransferredOverUrl"; //import { foo, bar } from "./valuesTransferredOverUrl";
//console.log(`Values passed by the main app in the URL parameter:`, { foo, bar }); //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 // 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 Register = lazy(() => import("./pages/Register"));
const RegisterUserProfile = lazy(() => import("./pages/RegisterUserProfile")); const RegisterUserProfile = lazy(() => import("./pages/RegisterUserProfile"));
const Terms = lazy(() => import("./pages/Terms")); const Terms = lazy(() => import("./pages/Terms"));
const MyExtraPage1 = lazy(() => import("./pages/MyExtraPage1")); const MyExtraPage1 = lazy(() => import("./pages/MyExtraPage1"));
const MyExtraPage2 = lazy(() => import("./pages/MyExtraPage2")); 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 // https://github.com/keycloak/keycloak/blob/11.0.3/themes/src/main/resources/theme/keycloak/login/theme.properties
const kcProps: KcProps = { const classes: PageProps<any, any>["classes"] = {
...defaultKcProps,
// NOTE: The classes are defined in ./KcApp.css // NOTE: The classes are defined in ./KcApp.css
// You can add your classes alongside thoses that are present in the default Keycloak theme... "kcHtmlClass": "my-root-class",
"kcHtmlClass": [...defaultKcProps.kcHtmlClass, "my-root-class"],
// ...or overwrite
"kcHeaderWrapperClass": "my-color my-font" "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. //NOTE: Locales not yet downloaded, we could as well display a loading progress but it's usually a matter of milliseconds.
return null; return null;
} }
/* /*
* Examples assuming i18n.currentLanguageTag === "en": * Examples assuming i18n.currentLanguageTag === "en":
* i18n.msg("access-denied") === <span>Access denied</span> * i18n.msg("access-denied") === <span>Access denied</span>
* i18n.msg("foo") === <span>foo in English</span> * i18n.msg("foo") === <span>foo in English</span>
*/ */
const pageProps: Omit<PageProps<any, typeof i18n>, "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 ( return (
<Suspense> <Suspense>
{(() => { {(() => {
switch (kcContext.pageId) { switch (kcContext.pageId) {
case "login.ftl": return <Login {...{ kcContext, ...pageProps }} />; case "login.ftl": return <Login {...{ kcContext, i18n, Template, classes, "doUseDefaultCss": true }} />;
case "register.ftl": return <Register {...{ kcContext, ...pageProps }} />; case "register.ftl": return <Register {...{ kcContext, i18n, Template, classes, "doUseDefaultCss": true }} />;
case "register-user-profile.ftl": return <RegisterUserProfile {...{ kcContext, ...pageProps }} /> case "register-user-profile.ftl": return <RegisterUserProfile {...{ kcContext, i18n, Template, classes, "doUseDefaultCss": true }} />
case "terms.ftl": return <Terms {...{ kcContext, ...pageProps }} />; case "terms.ftl": return <Terms {...{ kcContext, i18n, Template, classes, "doUseDefaultCss": true }} />;
case "my-extra-page-1.ftl": return <MyExtraPage1 {...{ kcContext, ...pageProps }} />; case "my-extra-page-1.ftl": return <MyExtraPage1 {...{ kcContext, i18n, Template, classes, "doUseDefaultCss": true }} />;
case "my-extra-page-2.ftl": return <MyExtraPage2 {...{ kcContext, ...pageProps }} />; case "my-extra-page-2.ftl": return <MyExtraPage2 {...{ kcContext, i18n, Template, classes, "doUseDefaultCss": true }} />;
// We choose to use the default Template for the Info page and to download the theme resources. // We choose to use the default Template for the Info page and to download the theme resources.
case "info.ftl": return <Info {...{ kcContext, ...pageProps}} Template={DefaultTemplate} doFetchDefaultThemeResources={true} />; case "info.ftl": return <Info {...{ kcContext, i18n, "Template": DefaultTemplate, classes, "doUseDefaultCss": true }} />;
default: return <Fallback {...{ kcContext, ...pageProps }} />; default: return <Fallback {...{ kcContext, i18n, "Template": DefaultTemplate, classes, "doUseDefaultCss": true }} />;
} }
})()} })()}
</Suspense> </Suspense>

View file

@ -1,13 +1,9 @@
// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/components/shared/Template.tsx // Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/Template.tsx
import { assert } from "keycloakify/tools/assert";
// You can replace all relative imports by cherry picking files from the keycloakify module. import { clsx } from "keycloakify/tools/clsx";
// For example, the following import: import { usePrepareTemplate } from "keycloakify/lib/usePrepareTemplate";
// import { assert } from "./tools/assert"; import { type TemplateProps, defaultTemplateClasses } from "keycloakify/TemplateProps";
// becomes: import { useGetClassName } from "keycloakify/lib/useGetClassName";
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";
import type { KcContext } from "./kcContext"; import type { KcContext } from "./kcContext";
import type { I18n } from "./i18n"; import type { I18n } from "./i18n";
@ -24,24 +20,29 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
infoNode = null, infoNode = null,
kcContext, kcContext,
i18n, i18n,
doFetchDefaultThemeResources, doUseDefaultCss,
stylesCommon, classes
styles,
scripts,
kcHtmlClass
} = props; } = props;
const { getClassName } = useGetClassName({
"defaultClasses": !doUseDefaultCss ? undefined : defaultTemplateClasses,
classes
});
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n; const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext; const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext;
const { isReady } = usePrepareTemplate({ const { isReady } = usePrepareTemplate({
doFetchDefaultThemeResources, "doFetchDefaultThemeResources": doUseDefaultCss,
stylesCommon,
styles,
scripts,
url, 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) { if (!isReady) {
@ -49,18 +50,18 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
} }
return ( return (
<div className={clsx(props.kcLoginClass)}> <div className={getClassName("kcLoginClass")}>
<div id="kc-header" className={clsx(props.kcHeaderClass)}> <div id="kc-header" className={getClassName("kcHeaderClass")}>
<div id="kc-header-wrapper" className={clsx(props.kcHeaderWrapperClass)}> <div id="kc-header-wrapper" className={getClassName("kcHeaderWrapperClass")}>
{msg("loginTitleHtml", realm.displayNameHtml)} {msg("loginTitleHtml", realm.displayNameHtml)}
</div> </div>
</div> </div>
<div className={clsx(props.kcFormCardClass, displayWide && props.kcFormCardAccountClass)}> <div className={clsx(getClassName("kcFormCardClass"), displayWide && getClassName("kcFormCardAccountClass"))}>
<header className={clsx(props.kcFormHeaderClass)}> <header className={getClassName("kcFormHeaderClass")}>
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && ( {realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
<div id="kc-locale"> <div id="kc-locale">
<div id="kc-locale-wrapper" className={clsx(props.kcLocaleWrapperClass)}> <div id="kc-locale-wrapper" className={getClassName("kcLocaleWrapperClass")}>
<div className="kc-dropdown" id="kc-locale-dropdown"> <div className="kc-dropdown" id="kc-locale-dropdown">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" id="kc-current-locale-link"> <a href="#" id="kc-current-locale-link">
@ -82,8 +83,8 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
)} )}
{!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? ( {!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? (
displayRequiredFields ? ( displayRequiredFields ? (
<div className={clsx(props.kcContentWrapperClass)}> <div className={getClassName("kcContentWrapperClass")}>
<div className={clsx(props.kcLabelWrapperClass, "subtitle")}> <div className={clsx(getClassName("kcLabelWrapperClass"), "subtitle")}>
<span className="subtitle"> <span className="subtitle">
<span className="required">*</span> <span className="required">*</span>
{msg("requiredFields")} {msg("requiredFields")}
@ -97,20 +98,20 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
<h1 id="kc-page-title">{headerNode}</h1> <h1 id="kc-page-title">{headerNode}</h1>
) )
) : displayRequiredFields ? ( ) : displayRequiredFields ? (
<div className={clsx(props.kcContentWrapperClass)}> <div className={getClassName("kcContentWrapperClass")}>
<div className={clsx(props.kcLabelWrapperClass, "subtitle")}> <div className={clsx(getClassName("kcLabelWrapperClass"), "subtitle")}>
<span className="subtitle"> <span className="subtitle">
<span className="required">*</span> {msg("requiredFields")} <span className="required">*</span> {msg("requiredFields")}
</span> </span>
</div> </div>
<div className="col-md-10"> <div className="col-md-10">
{showUsernameNode} {showUsernameNode}
<div className={clsx(props.kcFormGroupClass)}> <div className={getClassName("kcFormGroupClass")}>
<div id="kc-username"> <div id="kc-username">
<label id="kc-attempted-username">{auth?.attemptedUsername}</label> <label id="kc-attempted-username">{auth?.attemptedUsername}</label>
<a id="reset-login" href={url.loginRestartFlowUrl}> <a id="reset-login" href={url.loginRestartFlowUrl}>
<div className="kc-login-tooltip"> <div className="kc-login-tooltip">
<i className={clsx(props.kcResetFlowIcon)}></i> <i className={getClassName("kcResetFlowIcon")}></i>
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span> <span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
</div> </div>
</a> </a>
@ -121,12 +122,12 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
) : ( ) : (
<> <>
{showUsernameNode} {showUsernameNode}
<div className={clsx(props.kcFormGroupClass)}> <div className={getClassName("kcFormGroupClass")}>
<div id="kc-username"> <div id="kc-username">
<label id="kc-attempted-username">{auth?.attemptedUsername}</label> <label id="kc-attempted-username">{auth?.attemptedUsername}</label>
<a id="reset-login" href={url.loginRestartFlowUrl}> <a id="reset-login" href={url.loginRestartFlowUrl}>
<div className="kc-login-tooltip"> <div className="kc-login-tooltip">
<i className={clsx(props.kcResetFlowIcon)}></i> <i className={getClassName("kcResetFlowIcon")}></i>
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span> <span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
</div> </div>
</a> </a>
@ -140,10 +141,10 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
{/* App-initiated actions should not see warning messages about the need to complete the action during login. */} {/* App-initiated actions should not see warning messages about the need to complete the action during login. */}
{displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && ( {displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && (
<div className={clsx("alert", `alert-${message.type}`)}> <div className={clsx("alert", `alert-${message.type}`)}>
{message.type === "success" && <span className={clsx(props.kcFeedbackSuccessIcon)}></span>} {message.type === "success" && <span className={getClassName("kcFeedbackSuccessIcon")}></span>}
{message.type === "warning" && <span className={clsx(props.kcFeedbackWarningIcon)}></span>} {message.type === "warning" && <span className={getClassName("kcFeedbackWarningIcon")}></span>}
{message.type === "error" && <span className={clsx(props.kcFeedbackErrorIcon)}></span>} {message.type === "error" && <span className={getClassName("kcFeedbackErrorIcon")}></span>}
{message.type === "info" && <span className={clsx(props.kcFeedbackInfoIcon)}></span>} {message.type === "info" && <span className={getClassName("kcFeedbackInfoIcon")}></span>}
<span <span
className="kc-feedback-text" className="kc-feedback-text"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
@ -158,16 +159,24 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
id="kc-select-try-another-way-form" id="kc-select-try-another-way-form"
action={url.loginAction} action={url.loginAction}
method="post" method="post"
className={clsx(displayWide && props.kcContentWrapperClass)} className={clsx(displayWide && getClassName("kcContentWrapperClass"))}
> >
<div className={clsx(displayWide && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])}> <div
<div className={clsx(props.kcFormGroupClass)}> className={clsx(
displayWide && [getClassName("kcFormSocialAccountContentClass"), getClassName("kcFormSocialAccountClass")]
)}
>
<div className={getClassName("kcFormGroupClass")}>
<input type="hidden" name="tryAnotherWay" value="on" /> <input type="hidden" name="tryAnotherWay" value="on" />
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" id="try-another-way" onClick={() => { <a
document.forms["kc-select-try-another-way-form" as never].submit(); href="#"
return false; id="try-another-way"
}}> onClick={() => {
document.forms["kc-select-try-another-way-form" as never].submit();
return false;
}}
>
{msg("doTryAnotherWay")} {msg("doTryAnotherWay")}
</a> </a>
</div> </div>
@ -175,8 +184,8 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
</form> </form>
)} )}
{displayInfo && ( {displayInfo && (
<div id="kc-info" className={clsx(props.kcSignUpClass)}> <div id="kc-info" className={getClassName("kcSignUpClass")}>
<div id="kc-info-wrapper" className={clsx(props.kcInfoAreaWrapperClass)}> <div id="kc-info-wrapper" className={getClassName("kcInfoAreaWrapperClass")}>
{infoNode} {infoNode}
</div> </div>
</div> </div>

View file

@ -1,32 +1,24 @@
import { useI18n as useI18nBase } from "keycloakify"; import { createUseI18n } from "keycloakify";
type Props = Omit<Parameters<typeof useI18nBase>[0], "extraMessages">; export const { useI18n } = createUseI18n({
// NOTE: Here you can override the default i18n messages
export function useI18n(props: Props) { // or define new ones that, for example, you would have
const { kcContext } = props; // defined in the Keycloak admin UI for UserProfile
return useI18nBase({ // https://user-images.githubusercontent.com/6702424/182050652-522b6fe6-8ee5-49df-aca3-dba2d33f24a5.png
kcContext, en: {
// NOTE: Here you can override the default i18n messages alphanumericalCharsOnly: "Only alphanumerical characters",
// or define new ones that, for example, you would have gender: "Gender",
// defined in the Keycloak admin UI for UserProfile // Here we overwrite the default english value for the message "doForgotPassword"
// https://user-images.githubusercontent.com/6702424/182050652-522b6fe6-8ee5-49df-aca3-dba2d33f24a5.png // that is "Forgot Password?" see: https://github.com/InseeFrLab/keycloakify/blob/f0ae5ea908e0aa42391af323b6d5e2fd371af851/src/lib/i18n/generated_messages/18.0.1/login/en.ts#L17
"extraMessages": { doForgotPassword: "I forgot my password",
"en": { },
"alphanumericalCharsOnly": "Only alphanumerical characters", fr: {
"gender": "Gender", /* spell-checker: disable */
// Here we overwrite the default english value for the message "doForgotPassword" alphanumericalCharsOnly: "Caractère alphanumérique uniquement",
// that is "Forgot Password?" see: https://github.com/InseeFrLab/keycloakify/blob/f0ae5ea908e0aa42391af323b6d5e2fd371af851/src/lib/i18n/generated_messages/18.0.1/login/en.ts#L17 gender: "Genre",
"doForgotPassword": "I forgot my password", doForgotPassword: "J'ai oublié mon mot de passe"
}, /* spell-checker: enable */
"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<ReturnType<typeof useI18n>>; export type I18n = NonNullable<ReturnType<typeof useI18n>>;

View file

@ -1,4 +1,4 @@
import { getKcContext } from "keycloakify/lib/getKcContext"; import { getKcContext } from "keycloakify";
export type KcContextExtension = export type KcContextExtension =
// NOTE: A 'keycloakify' field must be added // NOTE: A 'keycloakify' field must be added

View file

@ -1,198 +1,204 @@
// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/pages/Login.tsx
import { useState, type FormEventHandler } from "react"; import { useState, type FormEventHandler } from "react";
// This is a copy paste from https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/Login.tsx import { clsx } from "keycloakify/tools/clsx";
// You can replace all relative imports by cherry picking files from the keycloakify module. import { useConstCallback } from "keycloakify/tools/useConstCallback";
// For example, the following import: import { type PageProps, defaultClasses } from "keycloakify/pages/PageProps";
// import { clsx } from "./tools/clsx"; import { useGetClassName } from "keycloakify/lib/useGetClassName";
// 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 type { KcContext } from "../kcContext"; import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
export default function Login(props: PageProps<Extract<KcContext, { pageId: "login.ftl"; }>, I18n>) { export default function Login(props: PageProps<Extract<KcContext, { pageId: "login.ftl" }>, I18n>) {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props; 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<FormEventHandler<HTMLFormElement>>(e => { const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
e.preventDefault();
setIsLoginButtonDisabled(true); const onSubmit = useConstCallback<FormEventHandler<HTMLFormElement>>(e => {
e.preventDefault();
const formElement = e.target as HTMLFormElement; setIsLoginButtonDisabled(true);
//NOTE: Even if we login with email Keycloak expect username and password in const formElement = e.target as HTMLFormElement;
//the POST request.
formElement.querySelector("input[name='email']")?.setAttribute("name", "username");
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 ( formElement.submit();
<Template });
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
displayInfo={social.displayInfo}
displayWide={realm.password && social.providers !== undefined}
headerNode={msg("doLogIn")}
formNode={
<div id="kc-form" className={clsx(realm.password && social.providers !== undefined && kcProps.kcContentWrapperClass)}>
<div
id="kc-form-wrapper"
className={clsx(
realm.password && social.providers && [kcProps.kcFormSocialAccountContentClass, kcProps.kcFormSocialAccountClass]
)}
>
{realm.password && (
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
<div className={clsx(kcProps.kcFormGroupClass)}>
{(() => {
const label = !realm.loginWithEmailAllowed
? "username"
: realm.registrationEmailAsUsername
? "email"
: "usernameOrEmail";
const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label; return (
<Template
{...{ kcContext, i18n, doUseDefaultCss, classes }}
displayInfo={social.displayInfo}
displayWide={realm.password && social.providers !== undefined}
headerNode={msg("doLogIn")}
formNode={
<div id="kc-form" className={clsx(realm.password && social.providers !== undefined && getClassName("kcContentWrapperClass"))}>
<div
id="kc-form-wrapper"
className={clsx(
realm.password &&
social.providers && [getClassName("kcFormSocialAccountContentClass"), getClassName("kcFormSocialAccountClass")]
)}
>
{realm.password && (
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
<div className={getClassName("kcFormGroupClass")}>
{(() => {
const label = !realm.loginWithEmailAllowed
? "username"
: realm.registrationEmailAsUsername
? "email"
: "usernameOrEmail";
return ( const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label;
<>
<label htmlFor={autoCompleteHelper} className={clsx(kcProps.kcLabelClass)}> return (
{msg(label)} <>
</label> <label htmlFor={autoCompleteHelper} className={getClassName("kcLabelClass")}>
<input {msg(label)}
tabIndex={1} </label>
id={autoCompleteHelper} <input
className={clsx(kcProps.kcInputClass)} tabIndex={1}
//NOTE: This is used by Google Chrome auto fill so we use it to tell id={autoCompleteHelper}
//the browser how to pre fill the form but before submit we put it back className={getClassName("kcInputClass")}
//to username because it is what keycloak expects. //NOTE: This is used by Google Chrome auto fill so we use it to tell
name={autoCompleteHelper} //the browser how to pre fill the form but before submit we put it back
defaultValue={login.username ?? ""} //to username because it is what keycloak expects.
type="text" name={autoCompleteHelper}
{...(usernameEditDisabled defaultValue={login.username ?? ""}
? { "disabled": true } type="text"
: { {...(usernameEditDisabled
"autoFocus": true, ? { "disabled": true }
"autoComplete": "off" : {
})} "autoFocus": true,
/> "autoComplete": "off"
</> })}
); />
})()} </>
</div> );
<div className={clsx(kcProps.kcFormGroupClass)}> })()}
<label htmlFor="password" className={clsx(kcProps.kcLabelClass)}> </div>
{msg("password")} <div className={getClassName("kcFormGroupClass")}>
</label> <label htmlFor="password" className={getClassName("kcLabelClass")}>
<input {msg("password")}
tabIndex={2} </label>
id="password" <input
className={clsx(kcProps.kcInputClass)} tabIndex={2}
name="password" id="password"
type="password" className={getClassName("kcInputClass")}
autoComplete="off" name="password"
/> type="password"
</div> autoComplete="off"
<div className={clsx(kcProps.kcFormGroupClass, kcProps.kcFormSettingClass)}> />
<div id="kc-form-options"> </div>
{realm.rememberMe && !usernameEditDisabled && ( <div className={clsx(getClassName("kcFormGroupClass"), getClassName("kcFormSettingClass"))}>
<div className="checkbox"> <div id="kc-form-options">
<label> {realm.rememberMe && !usernameEditDisabled && (
<input <div className="checkbox">
tabIndex={3} <label>
id="rememberMe" <input
name="rememberMe" tabIndex={3}
type="checkbox" id="rememberMe"
{...(login.rememberMe name="rememberMe"
? { type="checkbox"
"checked": true {...(login.rememberMe
} ? {
: {})} "checked": true
/> }
{msg("rememberMe")} : {})}
</label> />
</div> {msg("rememberMe")}
)} </label>
</div> </div>
<div className={clsx(kcProps.kcFormOptionsWrapperClass)}> )}
{realm.resetPasswordAllowed && ( </div>
<span> <div className={getClassName("kcFormOptionsWrapperClass")}>
<a tabIndex={5} href={url.loginResetCredentialsUrl}> {realm.resetPasswordAllowed && (
{msg("doForgotPassword")} <span>
</a> <a tabIndex={5} href={url.loginResetCredentialsUrl}>
</span> {msg("doForgotPassword")}
)} </a>
</div> </span>
</div> )}
<div id="kc-form-buttons" className={clsx(kcProps.kcFormGroupClass)}> </div>
<input </div>
type="hidden" <div id="kc-form-buttons" className={getClassName("kcFormGroupClass")}>
id="id-hidden-input" <input
name="credentialId" type="hidden"
{...(auth?.selectedCredential !== undefined id="id-hidden-input"
? { name="credentialId"
"value": auth.selectedCredential {...(auth?.selectedCredential !== undefined
} ? {
: {})} "value": auth.selectedCredential
/> }
<input : {})}
tabIndex={4} />
className={clsx( <input
kcProps.kcButtonClass, tabIndex={4}
kcProps.kcButtonPrimaryClass, className={clsx(
kcProps.kcButtonBlockClass, getClassName("kcButtonClass"),
kcProps.kcButtonLargeClass getClassName("kcButtonPrimaryClass"),
)} getClassName("kcButtonBlockClass"),
name="login" getClassName("kcButtonLargeClass")
id="kc-login" )}
type="submit" name="login"
value={msgStr("doLogIn")} id="kc-login"
disabled={isLoginButtonDisabled} type="submit"
/> value={msgStr("doLogIn")}
</div> disabled={isLoginButtonDisabled}
</form> />
)} </div>
</div> </form>
{realm.password && social.providers !== undefined && ( )}
<div id="kc-social-providers" className={clsx(kcProps.kcFormSocialAccountContentClass, kcProps.kcFormSocialAccountClass)}> </div>
<ul {realm.password && social.providers !== undefined && (
className={clsx( <div
kcProps.kcFormSocialAccountListClass, id="kc-social-providers"
social.providers.length > 4 && kcProps.kcFormSocialAccountDoubleListClass className={clsx(getClassName("kcFormSocialAccountContentClass"), getClassName("kcFormSocialAccountClass"))}
)} >
> <ul
{social.providers.map(p => ( className={clsx(
<li key={p.providerId} className={clsx(kcProps.kcFormSocialAccountListLinkClass)}> getClassName("kcFormSocialAccountListClass"),
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={clsx("zocial", p.providerId)}> social.providers.length > 4 && getClassName("kcFormSocialAccountDoubleListClass")
<span>{p.displayName}</span> )}
</a> >
</li> {social.providers.map(p => (
))} <li key={p.providerId} className={getClassName("kcFormSocialAccountListLinkClass")}>
</ul> <a href={p.loginUrl} id={`zocial-${p.alias}`} className={clsx("zocial", p.providerId)}>
</div> <span>{p.displayName}</span>
)} </a>
</div> </li>
} ))}
infoNode={ </ul>
realm.password && </div>
realm.registrationAllowed && )}
!registrationDisabled && ( </div>
<div id="kc-registration"> }
<span> infoNode={
{msg("noAccount")} realm.password &&
<a tabIndex={6} href={url.registrationUrl}> realm.registrationAllowed &&
{msg("doRegister")} !registrationDisabled && (
</a> <div id="kc-registration">
</span> <span>
</div> {msg("noAccount")}
) <a tabIndex={6} href={url.registrationUrl}>
} {msg("doRegister")}
/> </a>
); </span>
</div>
)
}
/>
);
} }

View file

@ -1,21 +1,21 @@
import type { PageProps } from "keycloakify"; import type { PageProps } from "keycloakify/pages/PageProps";
import type { KcContext } from "../kcContext"; import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
export default function MyExtraPage1(props: PageProps<Extract<KcContext, { pageId: "my-extra-page-1.ftl"; }>, I18n>) { export default function MyExtraPage1(props: PageProps<Extract<KcContext, { pageId: "my-extra-page-1.ftl"; }>, I18n>) {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
return ( return (
<Template <Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }} {...{ kcContext, i18n, doUseDefaultCss, classes }}
headerNode={<>Header <i>text</i></>} headerNode={<>Header <i>text</i></>}
formNode={ formNode={
<form> <form>
{/*...*/} {/*...*/}
</form> </form>
} }
infoNode={<span>footer</span> } infoNode={<span>footer</span>}
/> />
); );

View file

@ -1,17 +1,17 @@
import type { PageProps } from "keycloakify"; import type { PageProps } from "keycloakify/pages/PageProps";
import type { KcContext } from "../kcContext"; import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
export default function MyExtraPage1(props: PageProps<Extract<KcContext, { pageId: "my-extra-page-2.ftl"; }>, I18n>) { export default function MyExtraPage1(props: PageProps<Extract<KcContext, { pageId: "my-extra-page-2.ftl"; }>, I18n>) {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
// someCustomValue is declared by you in ../kcContext.ts // someCustomValue is declared by you in ../kcContext.ts
console.log(`TODO: Do something with: ${kcContext.someCustomValue}`); console.log(`TODO: Do something with: ${kcContext.someCustomValue}`);
return ( return (
<Template <Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }} {...{ kcContext, i18n, doUseDefaultCss, classes }}
headerNode={<>Header <i>text</i></>} headerNode={<>Header <i>text</i></>}
formNode={ formNode={
<form> <form>

View file

@ -1,74 +1,89 @@
// This is a copy paste from https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/Register.tsx // Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/pages/Register.tsx
// It is now up to us to implement a special behavior to leverage the non standard authorizedMailDomains
// provided by the plugin: https://github.com/micedre/keycloak-mail-whitelisting installed on our keycloak server. import { clsx } from "keycloakify/tools/clsx";
// Note that it is no longer recommended to use register.ftl, it's best to use register-user-profile.ftl import { type PageProps, defaultClasses } from "keycloakify/pages/PageProps";
// See: https://docs.keycloakify.dev/realtime-input-validation import { useGetClassName } from "keycloakify/lib/useGetClassName";
import { clsx } from "keycloakify/lib/tools/clsx";
import type { PageProps } from "keycloakify/lib/KcProps";
import type { KcContext } from "../kcContext"; import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
export default function Register(props: PageProps<Extract<KcContext, { pageId: "register.ftl"; }>, I18n>) { export default function Register(props: PageProps<Extract<KcContext, { pageId: "register.ftl" }>, I18n>) {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { getClassName } = useGetClassName({
"defaultClasses": !doUseDefaultCss ? undefined : defaultClasses,
classes
});
const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext; const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
const { msg, msgStr } = i18n; const { msg, msgStr } = i18n;
console.log(`NOTE: It is up to you do do something meaningful with ${kcContext.authorizedMailDomains}`);
return ( return (
<Template <Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }} {...{ kcContext, i18n, doUseDefaultCss, classes }}
headerNode={msg("registerTitle")} headerNode={msg("registerTitle")}
formNode={ formNode={
<form id="kc-register-form" className={clsx(kcProps.kcFormClass)} action={url.registrationAction} method="post"> <form id="kc-register-form" className={getClassName("kcFormClass")} action={url.registrationAction} method="post">
<div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("firstName", kcProps.kcFormGroupErrorClass))}> <div
<div className={clsx(kcProps.kcLabelWrapperClass)}> className={clsx(
<label htmlFor="firstName" className={clsx(kcProps.kcLabelClass)}> getClassName("kcFormGroupClass"),
messagesPerField.printIfExists("firstName", getClassName("kcFormGroupErrorClass"))
)}
>
<div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor="firstName" className={getClassName("kcLabelClass")}>
{msg("firstName")} {msg("firstName")}
</label> </label>
</div> </div>
<div className={clsx(kcProps.kcInputWrapperClass)}> <div className={getClassName("kcInputWrapperClass")}>
<input <input
type="text" type="text"
id="firstName" id="firstName"
className={clsx(kcProps.kcInputClass)} className={getClassName("kcInputClass")}
name="firstName" name="firstName"
defaultValue={register.formData.firstName ?? ""} defaultValue={register.formData.firstName ?? ""}
/> />
</div> </div>
</div> </div>
<div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("lastName", kcProps.kcFormGroupErrorClass))}> <div
<div className={clsx(kcProps.kcLabelWrapperClass)}> className={clsx(
<label htmlFor="lastName" className={clsx(kcProps.kcLabelClass)}> getClassName("kcFormGroupClass"),
messagesPerField.printIfExists("lastName", getClassName("kcFormGroupErrorClass"))
)}
>
<div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor="lastName" className={getClassName("kcLabelClass")}>
{msg("lastName")} {msg("lastName")}
</label> </label>
</div> </div>
<div className={clsx(kcProps.kcInputWrapperClass)}> <div className={getClassName("kcInputWrapperClass")}>
<input <input
type="text" type="text"
id="lastName" id="lastName"
className={clsx(kcProps.kcInputClass)} className={getClassName("kcInputClass")}
name="lastName" name="lastName"
defaultValue={register.formData.lastName ?? ""} defaultValue={register.formData.lastName ?? ""}
/> />
</div> </div>
</div> </div>
<div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("email", kcProps.kcFormGroupErrorClass))}> <div
<div className={clsx(kcProps.kcLabelWrapperClass)}> className={clsx(
<label htmlFor="email" className={clsx(kcProps.kcLabelClass)}> getClassName("kcFormGroupClass"),
messagesPerField.printIfExists("email", getClassName("kcFormGroupErrorClass"))
)}
>
<div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor="email" className={getClassName("kcLabelClass")}>
{msg("email")} {msg("email")}
</label> </label>
</div> </div>
<div className={clsx(kcProps.kcInputWrapperClass)}> <div className={getClassName("kcInputWrapperClass")}>
<input <input
type="text" type="text"
id="email" id="email"
className={clsx(kcProps.kcInputClass)} className={getClassName("kcInputClass")}
name="email" name="email"
defaultValue={register.formData.email ?? ""} defaultValue={register.formData.email ?? ""}
autoComplete="email" autoComplete="email"
@ -76,17 +91,22 @@ export default function Register(props: PageProps<Extract<KcContext, { pageId: "
</div> </div>
</div> </div>
{!realm.registrationEmailAsUsername && ( {!realm.registrationEmailAsUsername && (
<div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("username", kcProps.kcFormGroupErrorClass))}> <div
<div className={clsx(kcProps.kcLabelWrapperClass)}> className={clsx(
<label htmlFor="username" className={clsx(kcProps.kcLabelClass)}> getClassName("kcFormGroupClass"),
messagesPerField.printIfExists("username", getClassName("kcFormGroupErrorClass"))
)}
>
<div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor="username" className={getClassName("kcLabelClass")}>
{msg("username")} {msg("username")}
</label> </label>
</div> </div>
<div className={clsx(kcProps.kcInputWrapperClass)}> <div className={getClassName("kcInputWrapperClass")}>
<input <input
type="text" type="text"
id="username" id="username"
className={clsx(kcProps.kcInputClass)} className={getClassName("kcInputClass")}
name="username" name="username"
defaultValue={register.formData.username ?? ""} defaultValue={register.formData.username ?? ""}
autoComplete="username" autoComplete="username"
@ -97,18 +117,21 @@ export default function Register(props: PageProps<Extract<KcContext, { pageId: "
{passwordRequired && ( {passwordRequired && (
<> <>
<div <div
className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("password", kcProps.kcFormGroupErrorClass))} className={clsx(
getClassName("kcFormGroupClass"),
messagesPerField.printIfExists("password", getClassName("kcFormGroupErrorClass"))
)}
> >
<div className={clsx(kcProps.kcLabelWrapperClass)}> <div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor="password" className={clsx(kcProps.kcLabelClass)}> <label htmlFor="password" className={getClassName("kcLabelClass")}>
{msg("password")} {msg("password")}
</label> </label>
</div> </div>
<div className={clsx(kcProps.kcInputWrapperClass)}> <div className={getClassName("kcInputWrapperClass")}>
<input <input
type="password" type="password"
id="password" id="password"
className={clsx(kcProps.kcInputClass)} className={getClassName("kcInputClass")}
name="password" name="password"
autoComplete="new-password" autoComplete="new-password"
/> />
@ -117,44 +140,44 @@ export default function Register(props: PageProps<Extract<KcContext, { pageId: "
<div <div
className={clsx( className={clsx(
kcProps.kcFormGroupClass, getClassName("kcFormGroupClass"),
messagesPerField.printIfExists("password-confirm", kcProps.kcFormGroupErrorClass) messagesPerField.printIfExists("password-confirm", getClassName("kcFormGroupErrorClass"))
)} )}
> >
<div className={clsx(kcProps.kcLabelWrapperClass)}> <div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor="password-confirm" className={clsx(kcProps.kcLabelClass)}> <label htmlFor="password-confirm" className={getClassName("kcLabelClass")}>
{msg("passwordConfirm")} {msg("passwordConfirm")}
</label> </label>
</div> </div>
<div className={clsx(kcProps.kcInputWrapperClass)}> <div className={getClassName("kcInputWrapperClass")}>
<input type="password" id="password-confirm" className={clsx(kcProps.kcInputClass)} name="password-confirm" /> <input type="password" id="password-confirm" className={getClassName("kcInputClass")} name="password-confirm" />
</div> </div>
</div> </div>
</> </>
)} )}
{recaptchaRequired && ( {recaptchaRequired && (
<div className="form-group"> <div className="form-group">
<div className={clsx(kcProps.kcInputWrapperClass)}> <div className={getClassName("kcInputWrapperClass")}>
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey}></div> <div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey}></div>
</div> </div>
</div> </div>
)} )}
<div className={clsx(kcProps.kcFormGroupClass)}> <div className={getClassName("kcFormGroupClass")}>
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}> <div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
<div className={clsx(kcProps.kcFormOptionsWrapperClass)}> <div className={getClassName("kcFormOptionsWrapperClass")}>
<span> <span>
<a href={url.loginUrl}>{msg("backToLogin")}</a> <a href={url.loginUrl}>{msg("backToLogin")}</a>
</span> </span>
</div> </div>
</div> </div>
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}> <div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
<input <input
className={clsx( className={clsx(
kcProps.kcButtonClass, getClassName("kcButtonClass"),
kcProps.kcButtonPrimaryClass, getClassName("kcButtonPrimaryClass"),
kcProps.kcButtonBlockClass, getClassName("kcButtonBlockClass"),
kcProps.kcButtonLargeClass getClassName("kcButtonLargeClass")
)} )}
type="submit" type="submit"
value={msgStr("doRegister")} value={msgStr("doRegister")}
@ -166,4 +189,3 @@ export default function Register(props: PageProps<Extract<KcContext, { pageId: "
/> />
); );
} }

View file

@ -1,13 +1,20 @@
// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/RegisterUserProfile.tsx // Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/pages/RegisterUserProfile.tsx
import { useState } from "react"; import { useState } from "react";
import { clsx } from "keycloakify/lib/tools/clsx"; import { clsx } from "keycloakify/tools/clsx";
import { UserProfileFormFields } from "keycloakify/lib/pages/shared/UserProfileCommons"; import { UserProfileFormFields } from "./shared/UserProfileCommons";
import type { PageProps } from "keycloakify/lib/KcProps"; import { type PageProps, defaultClasses } from "keycloakify/pages/PageProps";
import { useGetClassName } from "keycloakify/lib/useGetClassName";
import type { KcContext } from "../kcContext"; import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
export default function RegisterUserProfile(props: PageProps<Extract<KcContext, { pageId: "register-user-profile.ftl" }>, I18n>) { export default function RegisterUserProfile(props: PageProps<Extract<KcContext, { pageId: "register-user-profile.ftl" }>, I18n>) {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { getClassName } = useGetClassName({
"defaultClasses": !doUseDefaultCss ? undefined : defaultClasses,
classes
});
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext; const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
@ -17,36 +24,41 @@ export default function RegisterUserProfile(props: PageProps<Extract<KcContext,
return ( return (
<Template <Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }} {...{ kcContext, i18n, doUseDefaultCss, classes }}
displayMessage={messagesPerField.exists("global")} displayMessage={messagesPerField.exists("global")}
displayRequiredFields={true} displayRequiredFields={true}
headerNode={msg("registerTitle")} headerNode={msg("registerTitle")}
formNode={ formNode={
<form id="kc-register-form" className={clsx(kcProps.kcFormClass)} action={url.registrationAction} method="post"> <form id="kc-register-form" className={getClassName("kcFormClass")} action={url.registrationAction} method="post">
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...kcProps} /> <UserProfileFormFields
kcContext={kcContext}
onIsFormSubmittableValueChange={setIsFomSubmittable}
i18n={i18n}
getClassName={getClassName}
/>
{recaptchaRequired && ( {recaptchaRequired && (
<div className="form-group"> <div className="form-group">
<div className={clsx(kcProps.kcInputWrapperClass)}> <div className={getClassName("kcInputWrapperClass")}>
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey} /> <div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey} />
</div> </div>
</div> </div>
)} )}
<div className={clsx(kcProps.kcFormGroupClass)} style={{ "marginBottom": 30 }}> <div className={getClassName("kcFormGroupClass")} style={{ "marginBottom": 30 }}>
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}> <div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
<div className={clsx(kcProps.kcFormOptionsWrapperClass)}> <div className={getClassName("kcFormOptionsWrapperClass")}>
<span> <span>
<a href={url.loginUrl}>{msg("backToLogin")}</a> <a href={url.loginUrl}>{msg("backToLogin")}</a>
</span> </span>
</div> </div>
</div> </div>
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}> <div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
<input <input
className={clsx( className={clsx(
kcProps.kcButtonClass, getClassName("kcButtonClass"),
kcProps.kcButtonPrimaryClass, getClassName("kcButtonPrimaryClass"),
kcProps.kcButtonBlockClass, getClassName("kcButtonBlockClass"),
kcProps.kcButtonLargeClass getClassName("kcButtonLargeClass")
)} )}
type="submit" type="submit"
value={msgStr("doRegister")} value={msgStr("doRegister")}

View file

@ -5,45 +5,28 @@
* in the KcApp.tsx * in the KcApp.tsx
* Example: https://github.com/garronej/keycloakify-starter/blob/a20c21b2aae7c6dc6dbea294f3d321955ddf9355/src/KcApp/KcApp.tsx#L14-L30 * Example: https://github.com/garronej/keycloakify-starter/blob/a20c21b2aae7c6dc6dbea294f3d321955ddf9355/src/KcApp/KcApp.tsx#L14-L30
*/ */
import {clsx} from "keycloakify/lib/tools/clsx"; import { clsx } from "keycloakify/tools/clsx";
import {useRerenderOnStateChange} from "evt/hooks"; import { useRerenderOnStateChange } from "evt/hooks";
import {Markdown} from "keycloakify/lib/tools/Markdown"; import { Markdown } from "keycloakify/tools/Markdown";
import {evtTermMarkdown, useDownloadTerms} from "keycloakify/lib/pages/Terms"; import { type PageProps, defaultClasses } from "keycloakify/pages/PageProps";
import tos_en_url from "../assets/tos_en.md"; import { useGetClassName } from "keycloakify/lib/useGetClassName";
import tos_fr_url from "../assets/tos_fr.md"; import { evtTermMarkdown } from "keycloakify/lib/useDownloadTerms";
import type {PageProps} from "keycloakify/lib/KcProps"; import type { KcContext } from "../kcContext";
import type {KcContext} from "../kcContext"; import type { I18n } from "../i18n";
import type {I18n} from "../i18n";
export default function Terms(props: PageProps<Extract<KcContext, { pageId: "terms.ftl"; }>, I18n>) { export default function Terms(props: PageProps<Extract<KcContext, { pageId: "terms.ftl" }>, I18n>) {
const {kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps} = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const {msg, msgStr} = i18n; const { getClassName } = useGetClassName({
"defaultClasses": !doUseDefaultCss ? undefined : defaultClasses,
useDownloadTerms({ classes
kcContext,
"downloadTermMarkdown": async ({currentLanguageTag}) => {
const resource = (() => {
switch (currentLanguageTag) {
case "fr":
return tos_fr_url;
default:
return tos_en_url;
}
})();
// webpack5 (used via storybook) loads markdown as string, not url
if (resource.includes("\n")) return resource
const response = await fetch(resource);
return response.text();
},
}); });
const { msg, msgStr } = i18n;
useRerenderOnStateChange(evtTermMarkdown); useRerenderOnStateChange(evtTermMarkdown);
const {url} = kcContext; const { url } = kcContext;
if (evtTermMarkdown.state === undefined) { if (evtTermMarkdown.state === undefined) {
return null; return null;
@ -51,21 +34,20 @@ export default function Terms(props: PageProps<Extract<KcContext, { pageId: "ter
return ( return (
<Template <Template
{...{kcContext, i18n, doFetchDefaultThemeResources, ...kcProps}} {...{ kcContext, i18n, doUseDefaultCss, classes }}
displayMessage={false} displayMessage={false}
headerNode={msg("termsTitle")} headerNode={msg("termsTitle")}
formNode={ formNode={
<> <>
<div id="kc-terms-text">{evtTermMarkdown.state && <div id="kc-terms-text">{evtTermMarkdown.state && <Markdown>{evtTermMarkdown.state}</Markdown>}</div>
<Markdown>{evtTermMarkdown.state}</Markdown>}</div>
<form className="form-actions" action={url.loginAction} method="POST"> <form className="form-actions" action={url.loginAction} method="POST">
<input <input
className={clsx( className={clsx(
kcProps.kcButtonClass, getClassName("kcButtonClass"),
kcProps.kcButtonClass, getClassName("kcButtonClass"),
kcProps.kcButtonClass, getClassName("kcButtonClass"),
kcProps.kcButtonPrimaryClass, getClassName("kcButtonPrimaryClass"),
kcProps.kcButtonLargeClass getClassName("kcButtonLargeClass")
)} )}
name="accept" name="accept"
id="kc-accept" id="kc-accept"
@ -73,14 +55,14 @@ export default function Terms(props: PageProps<Extract<KcContext, { pageId: "ter
value={msgStr("doAccept")} value={msgStr("doAccept")}
/> />
<input <input
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonDefaultClass, kcProps.kcButtonLargeClass)} className={clsx(getClassName("kcButtonClass"), getClassName("kcButtonDefaultClass"), getClassName("kcButtonLargeClass"))}
name="cancel" name="cancel"
id="kc-decline" id="kc-decline"
type="submit" type="submit"
value={msgStr("doDecline")} value={msgStr("doDecline")}
/> />
</form> </form>
<div className="clearfix"/> <div className="clearfix" />
</> </>
} }
/> />