Refactor terms

This commit is contained in:
Joseph Garrone 2024-05-08 19:48:16 +02:00
parent fdead071e7
commit 54b129630e
6 changed files with 82 additions and 99 deletions

View File

@ -5,7 +5,6 @@ import type { I18n } from "./i18n";
import type { KcContext } from "./kcContext"; import type { KcContext } from "./kcContext";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot"; import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields"; import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
import type { TermsAcceptanceProps } from "keycloakify/login/TermsAcceptance";
const Login = lazy(() => import("keycloakify/login/pages/Login")); const Login = lazy(() => import("keycloakify/login/pages/Login"));
const Register = lazy(() => import("keycloakify/login/pages/Register")); const Register = lazy(() => import("keycloakify/login/pages/Register"));
@ -35,7 +34,6 @@ const DeleteCredential = lazy(() => import("keycloakify/login/pages/DeleteCreden
type FallbackProps = PageProps<KcContext, I18n> & { type FallbackProps = PageProps<KcContext, I18n> & {
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>; UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
TermsAcceptance: LazyOrNot<(props: TermsAcceptanceProps) => JSX.Element | null>;
}; };
export default function Fallback(props: FallbackProps) { export default function Fallback(props: FallbackProps) {

View File

@ -1,81 +0,0 @@
import type { ClassKey } from "keycloakify/login/TemplateProps";
import { useRerenderOnStateChange } from "evt/hooks";
import { Markdown } from "keycloakify/tools/Markdown";
import { evtTermMarkdown } from "keycloakify/login/lib/useDownloadTerms";
import type { KcContext } from "keycloakify/login/kcContext/KcContext";
import type { I18n } from "./i18n";
export type TermsAcceptanceProps = {
kcContext: KcContextLike;
i18n: I18n;
getClassName: (classKey: ClassKey) => string;
};
type KcContextLike = {
termsAcceptanceRequired?: boolean;
messagesPerField: Pick<KcContext.Common["messagesPerField"], "existsError" | "get">;
};
export function TermsAcceptance(props: TermsAcceptanceProps) {
const {
kcContext: { termsAcceptanceRequired = false }
} = props;
if (!termsAcceptanceRequired) {
return null;
}
return <TermsAcceptanceEnabled {...props} />;
}
export function TermsAcceptanceEnabled(props: TermsAcceptanceProps) {
const {
i18n,
getClassName,
kcContext: { messagesPerField }
} = props;
const { msg } = i18n;
useRerenderOnStateChange(evtTermMarkdown);
const termMarkdown = evtTermMarkdown.state;
if (termMarkdown === undefined) {
return null;
}
return (
<>
<div className="form-group">
<div className={getClassName("kcInputWrapperClass")}>
{msg("termsTitle")}
<div id="kc-registration-terms-text">
<Markdown>{termMarkdown}</Markdown>
</div>
</div>
</div>
<div className="form-group">
<div className={getClassName("kcLabelWrapperClass")}>
<input
type="checkbox"
id="termsAccepted"
name="termsAccepted"
className={getClassName("kcCheckboxInputClass")}
aria-invalid={messagesPerField.existsError("termsAccepted")}
/>
<label htmlFor="termsAccepted" className={getClassName("kcLabelClass")}>
{msg("acceptTerms")}
</label>
</div>
{messagesPerField.existsError("termsAccepted") && (
<div className={getClassName("kcLabelWrapperClass")}>
<span id="input-error-terms-accepted" className={getClassName("kcInputErrorMessageClass")} aria-live="polite">
{messagesPerField.get("termsAccepted")}
</span>
</div>
)}
</div>
</>
);
}

View File

@ -187,6 +187,7 @@ export declare namespace KcContext {
* A Keycloak Java extension used as dependency in Keycloakify. * A Keycloak Java extension used as dependency in Keycloakify.
*/ */
passwordPolicies?: PasswordPolicies; passwordPolicies?: PasswordPolicies;
termsAcceptanceRequired?: boolean;
}; };
export type Info = Common & { export type Info = Common & {

View File

@ -5,9 +5,10 @@ import { useConst } from "keycloakify/tools/useConst";
import { useConstCallback } from "keycloakify/tools/useConstCallback"; import { useConstCallback } from "keycloakify/tools/useConstCallback";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { Evt } from "evt"; import { Evt } from "evt";
import { useRerenderOnStateChange } from "evt/hooks/useRerenderOnStateChange";
import { KcContext } from "../kcContext"; import { KcContext } from "../kcContext";
export const evtTermMarkdown = Evt.create<string | undefined>(undefined); const evtTermsMarkdown = Evt.create<string | undefined>(undefined);
export type KcContextLike = { export type KcContextLike = {
pageId: string; pageId: string;
@ -41,8 +42,16 @@ export function useDownloadTerms(params: {
useEffect(() => { useEffect(() => {
if (kcContext.pageId === "terms.ftl" || kcContext.termsAcceptanceRequired) { if (kcContext.pageId === "terms.ftl" || kcContext.termsAcceptanceRequired) {
downloadTermMarkdownMemoized(kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag).then( downloadTermMarkdownMemoized(kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag).then(
thermMarkdown => (evtTermMarkdown.state = thermMarkdown) thermMarkdown => (evtTermsMarkdown.state = thermMarkdown)
); );
} }
}, []); }, []);
} }
export function useTermsMarkdown() {
useRerenderOnStateChange(evtTermsMarkdown);
const termsMarkdown = evtTermsMarkdown.state;
return { termsMarkdown };
}

View File

@ -2,26 +2,26 @@ import { useState } from "react";
import { clsx } from "keycloakify/tools/clsx"; import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName"; import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import { useTermsMarkdown } from "keycloakify/login/lib/useDownloadTerms";
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
import { Markdown } from "keycloakify/tools/Markdown";
import type { KcContext } from "../kcContext"; import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
import type { TermsAcceptanceProps } from "../TermsAcceptance";
type RegisterProps = PageProps<Extract<KcContext, { pageId: "register.ftl" | "register-user-profile.ftl" }>, I18n> & { type RegisterProps = PageProps<Extract<KcContext, { pageId: "register.ftl" | "register-user-profile.ftl" }>, I18n> & {
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>; UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
TermsAcceptance: LazyOrNot<(props: TermsAcceptanceProps) => JSX.Element | null>;
}; };
export default function Register(props: RegisterProps) { export default function Register(props: RegisterProps) {
const { kcContext, i18n, doUseDefaultCss, Template, classes, UserProfileFormFields, TermsAcceptance } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes, UserProfileFormFields } = props;
const { getClassName } = useGetClassName({ const { getClassName } = useGetClassName({
doUseDefaultCss, doUseDefaultCss,
classes classes
}); });
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext; const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey, termsAcceptanceRequired } = kcContext;
const { msg, msgStr } = i18n; const { msg, msgStr } = i18n;
@ -39,7 +39,15 @@ export default function Register(props: RegisterProps) {
}} }}
onIsFormSubmittableValueChange={setIsFormSubmittable} onIsFormSubmittableValueChange={setIsFormSubmittable}
/> />
<TermsAcceptance {...{ kcContext, i18n, getClassName }} /> {termsAcceptanceRequired && (
<TermsAcceptance
{...{
i18n,
getClassName,
messagesPerField
}}
/>
)}
{recaptchaRequired && ( {recaptchaRequired && (
<div className="form-group"> <div className="form-group">
<div className={getClassName("kcInputWrapperClass")}> <div className={getClassName("kcInputWrapperClass")}>
@ -73,3 +81,54 @@ export default function Register(props: RegisterProps) {
</Template> </Template>
); );
} }
function TermsAcceptance(props: {
i18n: I18n;
getClassName: ReturnType<typeof useGetClassName>["getClassName"];
messagesPerField: Pick<KcContext.Common["messagesPerField"], "existsError" | "get">;
}) {
const { i18n, getClassName, messagesPerField } = props;
const { msg } = i18n;
// NOTE: Refer to https://docs.keycloakify.dev/terms-and-conditions to load your terms and conditions.
const { termsMarkdown } = useTermsMarkdown();
if (termsMarkdown === undefined) {
return null;
}
return (
<>
<div className="form-group">
<div className={getClassName("kcInputWrapperClass")}>
{msg("termsTitle")}
<div id="kc-registration-terms-text">
<Markdown>{termsMarkdown}</Markdown>
</div>
</div>
</div>
<div className="form-group">
<div className={getClassName("kcLabelWrapperClass")}>
<input
type="checkbox"
id="termsAccepted"
name="termsAccepted"
className={getClassName("kcCheckboxInputClass")}
aria-invalid={messagesPerField.existsError("termsAccepted")}
/>
<label htmlFor="termsAccepted" className={getClassName("kcLabelClass")}>
{msg("acceptTerms")}
</label>
</div>
{messagesPerField.existsError("termsAccepted") && (
<div className={getClassName("kcLabelWrapperClass")}>
<span id="input-error-terms-accepted" className={getClassName("kcInputErrorMessageClass")} aria-live="polite">
{messagesPerField.get("termsAccepted")}
</span>
</div>
)}
</div>
</>
);
}

View File

@ -1,9 +1,8 @@
import { clsx } from "keycloakify/tools/clsx"; import { clsx } from "keycloakify/tools/clsx";
import { useRerenderOnStateChange } from "evt/hooks";
import { Markdown } from "keycloakify/tools/Markdown"; import { Markdown } from "keycloakify/tools/Markdown";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName"; import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import { evtTermMarkdown } from "keycloakify/login/lib/useDownloadTerms"; import { useTermsMarkdown } from "keycloakify/login/lib/useDownloadTerms";
import type { KcContext } from "../kcContext"; import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
@ -17,20 +16,18 @@ export default function Terms(props: PageProps<Extract<KcContext, { pageId: "ter
const { msg, msgStr } = i18n; const { msg, msgStr } = i18n;
useRerenderOnStateChange(evtTermMarkdown);
const { url } = kcContext; const { url } = kcContext;
const termMarkdown = evtTermMarkdown.state; const { termsMarkdown } = useTermsMarkdown();
if (termMarkdown === undefined) { if (termsMarkdown === undefined) {
return null; return null;
} }
return ( return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} displayMessage={false} headerNode={msg("termsTitle")}> <Template {...{ kcContext, i18n, doUseDefaultCss, classes }} displayMessage={false} headerNode={msg("termsTitle")}>
<div id="kc-terms-text"> <div id="kc-terms-text">
<Markdown>{termMarkdown}</Markdown> <Markdown>{termsMarkdown}</Markdown>
</div> </div>
<form className="form-actions" action={url.loginAction} method="POST"> <form className="form-actions" action={url.loginAction} method="POST">
<input <input